(NUM_TRUSTS_TO_CACHE) {
+ @Override
+ protected FingerprintStatus create(String fingerprint) {
+ return mXmppConnectionService.databaseBackend.getFingerprintStatus(
+ account, fingerprint);
+ }
+ };
+
+ private static IdentityKeyPair generateIdentityKeyPair() {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair...");
+ ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
+ return new IdentityKeyPair(
+ new IdentityKey(identityKeyPairKeys.getPublicKey()),
+ identityKeyPairKeys.getPrivateKey());
+ }
+
+ private static int generateRegistrationId() {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID...");
+ return KeyHelper.generateRegistrationId(true);
+ }
+
+ public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
+ this.account = account;
+ this.mXmppConnectionService = service;
+ this.localRegistrationId = loadRegistrationId();
+ this.currentPreKeyId = loadCurrentPreKeyId();
+ }
+
+ public int getCurrentPreKeyId() {
+ return currentPreKeyId;
+ }
+
+ // --------------------------------------
+ // IdentityKeyStore
+ // --------------------------------------
+
+ private IdentityKeyPair loadIdentityKeyPair() {
+ synchronized (mXmppConnectionService) {
+ IdentityKeyPair ownKey =
+ mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
+
+ if (ownKey != null) {
+ return ownKey;
+ } else {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Could not retrieve own IdentityKeyPair");
+ ownKey = generateIdentityKeyPair();
+ mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
+ }
+ return ownKey;
+ }
+ }
+
+ private int loadRegistrationId() {
+ return loadRegistrationId(false);
+ }
+
+ private int loadRegistrationId(boolean regenerate) {
+ String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
+ int reg_id;
+ if (!regenerate && regIdString != null) {
+ reg_id = Integer.valueOf(regIdString);
+ } else {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Could not retrieve axolotl registration id for account "
+ + account.getJid());
+ reg_id = generateRegistrationId();
+ boolean success =
+ this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
+ if (success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Failed to write new key to the database!");
+ }
+ }
+ return reg_id;
+ }
+
+ private int loadCurrentPreKeyId() {
+ String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
+ int prekey_id;
+ if (prekeyIdString != null) {
+ prekey_id = Integer.valueOf(prekeyIdString);
+ } else {
+ Log.w(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Could not retrieve current prekey id for account "
+ + account.getJid());
+ prekey_id = 0;
+ }
+ return prekey_id;
+ }
+
+ public void regenerate() {
+ mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
+ trustCache.evictAll();
+ account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
+ identityKeyPair = loadIdentityKeyPair();
+ localRegistrationId = loadRegistrationId(true);
+ currentPreKeyId = 0;
+ mXmppConnectionService.updateAccountUi();
+ }
+
+ /**
+ * Get the local client's identity key pair.
+ *
+ * @return The local client's persistent identity key pair.
+ */
+ @Override
+ public IdentityKeyPair getIdentityKeyPair() {
+ if (identityKeyPair == null) {
+ identityKeyPair = loadIdentityKeyPair();
+ }
+ return identityKeyPair;
+ }
+
+ /**
+ * Return the local client's registration ID.
+ *
+ * Clients should maintain a registration ID, a random number between 1 and 16380 that's
+ * generated once at install time.
+ *
+ * @return the local client's registration ID.
+ */
+ @Override
+ public int getLocalRegistrationId() {
+ return localRegistrationId;
+ }
+
+ /**
+ * Save a remote client's identity key
+ *
+ *
Store a remote client's identity key as trusted.
+ *
+ * @param address The address of the remote client.
+ * @param identityKey The remote client's identity key.
+ * @return true on success
+ */
+ @Override
+ public boolean saveIdentity(
+ final SignalProtocolAddress address, final IdentityKey identityKey) {
+ if (!mXmppConnectionService
+ .databaseBackend
+ .loadIdentityKeys(account, address.getName())
+ .contains(identityKey)) {
+ String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize());
+ FingerprintStatus status = getFingerprintStatus(fingerprint);
+ if (status == null) {
+ if (mXmppConnectionService.getAppSettings().isBTBVEnabled()
+ && !account.getAxolotlService().hasVerifiedKeys(address.getName())) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": blindly trusted "
+ + fingerprint
+ + " of "
+ + address.getName());
+ status = FingerprintStatus.createActiveTrusted();
+ } else {
+ status = FingerprintStatus.createActiveUndecided();
+ }
+ } else {
+ status = status.toActive();
+ }
+ mXmppConnectionService.databaseBackend.storeIdentityKey(
+ account, address.getName(), identityKey, status);
+ trustCache.remove(fingerprint);
+ }
+ return true;
+ }
+
+ /**
+ * Verify a remote client's identity key.
+ *
+ *
Determine whether a remote client's identity is trusted. Convention is that the TextSecure
+ * protocol is 'trust on first use.' This means that an identity key is considered 'trusted' if
+ * there is no entry for the recipient in the local store, or if it matches the saved key for a
+ * recipient in the local store. Only if it mismatches an entry in the local store is it
+ * considered 'untrusted.'
+ *
+ * @param identityKey The identity key to verify.
+ * @return true if trusted, false if untrusted.
+ */
+ @Override
+ public boolean isTrustedIdentity(
+ SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
+ return true;
+ }
+
+ public FingerprintStatus getFingerprintStatus(String fingerprint) {
+ return (fingerprint == null) ? null : trustCache.get(fingerprint);
+ }
+
+ public void setFingerprintStatus(String fingerprint, FingerprintStatus status) {
+ mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status);
+ trustCache.remove(fingerprint);
+ }
+
+ public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) {
+ mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(
+ account, fingerprint, x509Certificate);
+ }
+
+ public X509Certificate getFingerprintCertificate(String fingerprint) {
+ return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(
+ account, fingerprint);
+ }
+
+ public Set getContactKeysWithTrust(String bareJid, FingerprintStatus status) {
+ return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status);
+ }
+
+ public long getContactNumTrustedKeys(String bareJid) {
+ return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
+ }
+
+ // --------------------------------------
+ // SessionStore
+ // --------------------------------------
+
+ /**
+ * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId
+ * tuple, or a new SessionRecord if one does not currently exist.
+ *
+ * It is important that implementations return a copy of the current durable information. The
+ * returned SessionRecord may be modified, but those changes should not have an effect on the
+ * durable session state (what is returned by subsequent calls to this method) without the store
+ * method being called here first.
+ *
+ * @param address The name and device ID of the remote client.
+ * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or a
+ * new SessionRecord if one does not currently exist.
+ */
+ @Override
+ public SessionRecord loadSession(SignalProtocolAddress address) {
+ SessionRecord session =
+ mXmppConnectionService.databaseBackend.loadSession(this.account, address);
+ return (session != null) ? session : new SessionRecord();
+ }
+
+ /**
+ * Returns all known devices with active sessions for a recipient
+ *
+ * @param name the name of the client.
+ * @return all known sub-devices with active sessions.
+ */
+ @Override
+ public List getSubDeviceSessions(String name) {
+ return mXmppConnectionService.databaseBackend.getSubDeviceSessions(
+ account, new SignalProtocolAddress(name, 0));
+ }
+
+ public List getKnownAddresses() {
+ return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account);
+ }
+
+ /**
+ * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @param record the current SessionRecord for the remote client.
+ */
+ @Override
+ public void storeSession(SignalProtocolAddress address, SessionRecord record) {
+ mXmppConnectionService.databaseBackend.storeSession(account, address, record);
+ }
+
+ /**
+ * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId
+ * tuple.
+ *
+ * @param address the address of the remote client.
+ * @return true if a {@link SessionRecord} exists, false otherwise.
+ */
+ @Override
+ public boolean containsSession(SignalProtocolAddress address) {
+ return mXmppConnectionService.databaseBackend.containsSession(account, address);
+ }
+
+ /**
+ * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ */
+ @Override
+ public void deleteSession(SignalProtocolAddress address) {
+ mXmppConnectionService.databaseBackend.deleteSession(account, address);
+ }
+
+ /**
+ * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
+ *
+ * @param name the name of the remote client.
+ */
+ @Override
+ public void deleteAllSessions(String name) {
+ SignalProtocolAddress address = new SignalProtocolAddress(name, 0);
+ mXmppConnectionService.databaseBackend.deleteAllSessions(account, address);
+ }
+
+ // --------------------------------------
+ // PreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the local PreKeyRecord.
+ * @return the corresponding PreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
+ */
+ @Override
+ public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
+ PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
+ if (record == null) {
+ throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
+ }
+ return record;
+ }
+
+ /**
+ * Store a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the PreKeyRecord to store.
+ * @param record the PreKeyRecord.
+ */
+ @Override
+ public void storePreKey(int preKeyId, PreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storePreKey(account, record);
+ currentPreKeyId = preKeyId;
+ boolean success =
+ this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
+ if (success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Failed to write new prekey id to the database!");
+ }
+ }
+
+ /**
+ * @param preKeyId A PreKeyRecord ID.
+ * @return true if the store has a record for the preKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsPreKey(int preKeyId) {
+ return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
+ }
+
+ /**
+ * Delete a PreKeyRecord from local storage.
+ *
+ * @param preKeyId The ID of the PreKeyRecord to remove.
+ */
+ @Override
+ public void removePreKey(int preKeyId) {
+ Log.d(Config.LOGTAG, "mark prekey for removal " + preKeyId);
+ synchronized (preKeysMarkedForRemoval) {
+ preKeysMarkedForRemoval.add(preKeyId);
+ }
+ }
+
+ public boolean flushPreKeys() {
+ Log.d(Config.LOGTAG, "flushing pre keys");
+ int count = 0;
+ synchronized (preKeysMarkedForRemoval) {
+ for (Integer preKeyId : preKeysMarkedForRemoval) {
+ count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
+ }
+ preKeysMarkedForRemoval.clear();
+ }
+ return count > 0;
+ }
+
+ // --------------------------------------
+ // SignedPreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
+ * @return the corresponding SignedPreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
+ */
+ @Override
+ public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
+ SignedPreKeyRecord record =
+ mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
+ if (record == null) {
+ throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
+ }
+ return record;
+ }
+
+ /**
+ * Load all local SignedPreKeyRecords.
+ *
+ * @return All stored SignedPreKeyRecords.
+ */
+ @Override
+ public List loadSignedPreKeys() {
+ return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
+ }
+
+ public int getSignedPreKeysCount() {
+ return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account);
+ }
+
+ /**
+ * Store a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
+ * @param record the SignedPreKeyRecord.
+ */
+ @Override
+ public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
+ }
+
+ /**
+ * @param signedPreKeyId A SignedPreKeyRecord ID.
+ * @return true if the store has a record for the signedPreKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsSignedPreKey(int signedPreKeyId) {
+ return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
+ }
+
+ /**
+ * Delete a SignedPreKeyRecord from local storage.
+ *
+ * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
+ */
+ @Override
+ public void removeSignedPreKey(int signedPreKeyId) {
+ mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
+ }
+
+ public void preVerifyFingerprint(Account account, String name, String fingerprint) {
+ mXmppConnectionService.databaseBackend.storePreVerification(
+ account, name, fingerprint, FingerprintStatus.createInactiveVerified());
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index d7507cec87e7a803cd1a0e57fd6d23f72e99f669..61b0d627c367c85a7bebefbc9f88405aeb060311 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -161,13 +161,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
this.password = password;
this.options = options;
this.rosterVersion = rosterVersion;
- JSONObject tmp;
- try {
- tmp = new JSONObject(keys);
- } catch (JSONException e) {
- tmp = new JSONObject();
- }
- this.keys = tmp;
+ this.keys = parseKeys(keys);
this.avatar = avatar;
this.displayName = displayName;
this.hostname = hostname;
@@ -180,6 +174,17 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
this.fastToken = fastToken;
}
+ public static JSONObject parseKeys(final String keys) {
+ if (Strings.isNullOrEmpty(keys)) {
+ return new JSONObject();
+ }
+ try {
+ return new JSONObject(keys);
+ } catch (final JSONException e) {
+ return new JSONObject();
+ }
+ }
+
public static Account fromCursor(final Cursor cursor) {
final Jid jid;
try {
diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
index 072b4fd065248948a6c473d053b6e08fcc6fd439..841bcee515c22dc11605881495efeaace0ab0eb9 100644
--- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
+++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
@@ -1,94 +1,87 @@
package eu.siacs.conversations.entities;
import android.util.Log;
-
-import java.io.File;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.MimeUtils;
+import java.io.File;
public class DownloadableFile extends File {
- private static final long serialVersionUID = 2247012619505115863L;
-
- private long expectedSize = 0;
- private byte[] sha1sum;
- private byte[] aeskey;
- private byte[] iv;
-
- public DownloadableFile(final File parent, final String file) {
- super(parent, file);
- }
-
- public DownloadableFile(String path) {
- super(path);
- }
-
- public long getSize() {
- return super.length();
- }
-
- public long getExpectedSize() {
- return this.expectedSize;
- }
-
- public String getMimeType() {
- String path = this.getAbsolutePath();
- int start = path.lastIndexOf('.') + 1;
- if (start < path.length()) {
- String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
- return mime == null ? "" : mime;
- } else {
- return "";
- }
- }
-
- public void setExpectedSize(long size) {
- this.expectedSize = size;
- }
-
- public byte[] getSha1Sum() {
- return this.sha1sum;
- }
-
- public void setSha1Sum(byte[] sum) {
- this.sha1sum = sum;
- }
-
- public void setKeyAndIv(byte[] keyIvCombo) {
- // originally, we used a 16 byte IV, then found for aes-gcm a 12 byte IV is ideal
- // this code supports reading either length, with sending 12 byte IV to be done in future
- if (keyIvCombo.length == 48) {
- this.aeskey = new byte[32];
- this.iv = new byte[16];
- System.arraycopy(keyIvCombo, 0, this.iv, 0, 16);
- System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32);
- } else if (keyIvCombo.length == 44) {
- this.aeskey = new byte[32];
- this.iv = new byte[12];
- System.arraycopy(keyIvCombo, 0, this.iv, 0, 12);
- System.arraycopy(keyIvCombo, 12, this.aeskey, 0, 32);
- } else if (keyIvCombo.length >= 32) {
- this.aeskey = new byte[32];
- this.iv = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
- System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
- }
- Log.d(Config.LOGTAG,"using "+this.iv.length+"-byte IV for file transmission");
- }
-
- public void setKey(byte[] key) {
- this.aeskey = key;
- }
-
- public void setIv(byte[] iv) {
- this.iv = iv;
- }
-
- public byte[] getKey() {
- return this.aeskey;
- }
-
- public byte[] getIv() {
- return this.iv;
- }
+ private static final long serialVersionUID = 2247012619505115863L;
+
+ private long expectedSize = 0;
+ private byte[] aeskey;
+ private byte[] iv;
+
+ public DownloadableFile(final File parent, final String file) {
+ super(parent, file);
+ }
+
+ public DownloadableFile(String path) {
+ super(path);
+ }
+
+ public long getSize() {
+ return super.length();
+ }
+
+ public long getExpectedSize() {
+ return this.expectedSize;
+ }
+
+ public String getMimeType() {
+ String path = this.getAbsolutePath();
+ int start = path.lastIndexOf('.') + 1;
+ if (start < path.length()) {
+ String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
+ return mime == null ? "" : mime;
+ } else {
+ return "";
+ }
+ }
+
+ public void setExpectedSize(long size) {
+ this.expectedSize = size;
+ }
+
+ public void setKeyAndIv(byte[] keyIvCombo) {
+ // originally, we used a 16 byte IV, then found for aes-gcm a 12 byte IV is ideal
+ // this code supports reading either length, with sending 12 byte IV to be done in future
+ if (keyIvCombo.length == 48) {
+ this.aeskey = new byte[32];
+ this.iv = new byte[16];
+ System.arraycopy(keyIvCombo, 0, this.iv, 0, 16);
+ System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32);
+ } else if (keyIvCombo.length == 44) {
+ this.aeskey = new byte[32];
+ this.iv = new byte[12];
+ System.arraycopy(keyIvCombo, 0, this.iv, 0, 12);
+ System.arraycopy(keyIvCombo, 12, this.aeskey, 0, 32);
+ } else if (keyIvCombo.length >= 32) {
+ this.aeskey = new byte[32];
+ this.iv =
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0xf
+ };
+ System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
+ }
+ Log.d(Config.LOGTAG, "using " + this.iv.length + "-byte IV for file transmission");
+ }
+
+ public void setKey(byte[] key) {
+ this.aeskey = key;
+ }
+
+ public void setIv(byte[] iv) {
+ this.iv = iv;
+ }
+
+ public byte[] getKey() {
+ return this.aeskey;
+ }
+
+ public byte[] getIv() {
+ return this.iv;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index 112f3c40fab9c80f97d63d0a741c296ecfa4f2c8..b433c1ccfe3306cdf9014813bce31b1ab80d6c5f 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -48,19 +48,18 @@ import java.util.stream.Collectors;
import io.ipfs.cid.Cid;
+import de.gultsch.common.Patterns;
+import de.gultsch.common.Linkify;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.http.URL;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.ui.text.FixedURLSpan;
-import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons;
-import eu.siacs.conversations.utils.GeoHelper;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.StringUtils;
@@ -1249,7 +1248,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
SpannableStringBuilder text = new SpannableStringBuilder(
getBody(true).replaceAll("^>.*", "") // Remove quotes
);
- return MyLinkify.extractLinks(text).stream().map((url) -> {
+ return Linkify.extractLinks(text).stream().map((url) -> {
try {
return new URI(url);
} catch (final URISyntaxException e) {
@@ -1400,7 +1399,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public synchronized boolean isGeoUri() {
if (isGeoUri == null) {
- isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
+ isGeoUri = Patterns.URI_GEO.matcher(body).matches();
}
return isGeoUri;
}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
index 7a2b800723e35243bd8ae1cf056815dfdae5faad..33899242c1ff659db71d57972d278c222fefe859 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -8,23 +8,15 @@ import android.util.Log;
import androidx.core.util.Consumer;
+import de.gultsch.common.TrustManagers;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
-import eu.siacs.conversations.crypto.TrustManagers;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.TLSSocketFactory;
-
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.ResponseBody;
-
-import org.apache.http.conn.ssl.StrictHostnameVerifier;
-
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
@@ -40,9 +32,13 @@ import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.ResponseBody;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
public class HttpConnectionManager extends AbstractConnectionManager {
@@ -54,18 +50,20 @@ public class HttpConnectionManager extends AbstractConnectionManager {
private static final OkHttpClient OK_HTTP_CLIENT;
static {
- OK_HTTP_CLIENT = new OkHttpClient.Builder()
- .addInterceptor(chain -> {
- final Request original = chain.request();
- final Request modified = original.newBuilder()
- .header("User-Agent", getUserAgent())
- .build();
- return chain.proceed(modified);
- })
- .build();
+ OK_HTTP_CLIENT =
+ new OkHttpClient.Builder()
+ .addInterceptor(
+ chain -> {
+ final Request original = chain.request();
+ final Request modified =
+ original.newBuilder()
+ .header("User-Agent", getUserAgent())
+ .build();
+ return chain.proceed(modified);
+ })
+ .build();
}
-
public static String getUserAgent() {
return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME);
}
@@ -77,7 +75,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
public static Proxy getProxy() {
final InetAddress localhost;
try {
- localhost = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
+ localhost = InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
} catch (final UnknownHostException e) {
throw new IllegalStateException(e);
}
@@ -100,7 +98,10 @@ public class HttpConnectionManager extends AbstractConnectionManager {
synchronized (this.downloadConnections) {
for (HttpDownloadConnection connection : this.downloadConnections) {
if (connection.getMessage() == message) {
- Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": download already in progress");
+ Log.d(
+ Config.LOGTAG,
+ message.getConversation().getAccount().getJid().asBareJid()
+ + ": download already in progress");
return;
}
}
@@ -118,11 +119,18 @@ public class HttpConnectionManager extends AbstractConnectionManager {
synchronized (this.uploadConnections) {
for (HttpUploadConnection connection : this.uploadConnections) {
if (connection.getMessage() == message) {
- Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": upload already in progress");
+ Log.d(
+ Config.LOGTAG,
+ message.getConversation().getAccount().getJid().asBareJid()
+ + ": upload already in progress");
return;
}
}
- HttpUploadConnection connection = new HttpUploadConnection(message, Method.determine(message.getConversation().getAccount()), this, cb);
+ HttpUploadConnection connection =
+ new HttpUploadConnection(
+ message,
+ Method.determine(message.getConversation().getAccount()),
+ this, cb);
connection.init(delay);
this.uploadConnections.add(connection);
}
@@ -144,7 +152,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
return buildHttpClient(url, account, 30, interactive);
}
- public OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) {
+ public OkHttpClient buildHttpClient(
+ final HttpUrl url, final Account account, int readTimeout, boolean interactive) {
final String slotHostname = url.host();
final boolean onionSlot = slotHostname.endsWith(".onion");
final OkHttpClient.Builder builder = newBuilder(mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot);
@@ -161,7 +170,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive();
}
try {
- final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM);
+ final SSLSocketFactory sf =
+ new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
builder.sslSocketFactory(sf, trustManager);
builder.hostnameVerifier(new StrictHostnameVerifier());
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
@@ -222,20 +232,15 @@ public class HttpConnectionManager extends AbstractConnectionManager {
public static OkHttpClient okHttpClient(final Context context) {
final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
try {
- final X509TrustManager trustManager;
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
- trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
- } else {
- trustManager = TrustManagers.createDefaultTrustManager();
- }
+ final X509TrustManager trustManager = TrustManagers.createForAndroidVersion(context);
final SSLSocketFactory socketFactory =
new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
builder.sslSocketFactory(socketFactory, trustManager);
} catch (final IOException
- | KeyManagementException
- | NoSuchAlgorithmException
- | KeyStoreException
- | CertificateException e) {
+ | KeyManagementException
+ | NoSuchAlgorithmException
+ | KeyStoreException
+ | CertificateException e) {
Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt");
throw new RuntimeException(e);
}
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
index cd3de2a65607940368c91a1456ab8afe3a4b0420..4a4a0dbd7fd1f5a86699444523096e98a33f6b50 100644
--- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -95,6 +95,13 @@ public class PresenceParser extends AbstractParser
&& jid.equals(
Jid.Invalid.getNullForInvalid(
item.getAttributeAsJid("jid"))))) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": got self-presence from "
+ + user.getFullJid()
+ + ". occupant-id="
+ + occupantId);
if (mucOptions.setOnline()) {
mXmppConnectionService.getAvatarService().clear(mucOptions);
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 120f75ca0d8729f1bd7ab7b9b2ce92a1217744d7..fddaf86e3cb88c4d365fcc65237665150829981e 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -1880,7 +1880,7 @@ public class FileBackend {
return Uri.fromFile(getAvatarFile(avatar));
}
- public Drawable cropCenterSquareDrawable(Uri image, int size) {
+ public Drawable cropCenterSquareDrawable(final Uri image, final int size) {
if (android.os.Build.VERSION.SDK_INT >= 28) {
try {
ImageDecoder.Source source = ImageDecoder.createSource(mXmppConnectionService.getContentResolver(), image);
@@ -1904,34 +1904,36 @@ public class FileBackend {
}
}
- public Bitmap cropCenterSquare(Uri image, int size) {
+ public Bitmap cropCenterSquare(final Uri image, final int size) {
if (image == null) {
return null;
}
- InputStream is = null;
+ final BitmapFactory.Options options = new BitmapFactory.Options();
try {
- BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image, size);
- is = openInputStream(image);
+ } catch (final IOException | SecurityException e) {
+ Log.d(Config.LOGTAG, "unable to calculate sample size for " + image, e);
+ return null;
+ }
+ try (final InputStream is =
+ openInputStream(image)) {
if (is == null) {
return null;
}
- Bitmap input = BitmapFactory.decodeStream(is, null, options);
- if (input == null) {
+ final var originalBitmap = BitmapFactory.decodeStream(is, null, options);
+ if (originalBitmap == null) {
return null;
} else {
- input = rotate(input, getRotation(image));
- return cropCenterSquare(input, size);
+ final var bitmap = rotate(originalBitmap, getRotation(image));
+ return cropCenterSquare(bitmap, size);
}
- } catch (SecurityException | IOException e) {
- Log.d(Config.LOGTAG, "unable to open file " + image.toString(), e);
+ } catch (final SecurityException | IOException e) {
+ Log.d(Config.LOGTAG, "unable to open file " + image, e);
return null;
- } finally {
- close(is);
}
}
- public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
+ public Bitmap cropCenter(final Uri image, final int newHeight, final int newWidth) {
if (image == null) {
return null;
}
@@ -1939,7 +1941,7 @@ public class FileBackend {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth));
- is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ is = openInputStream(image);
if (is == null) {
return null;
}
@@ -1995,14 +1997,14 @@ public class FileBackend {
return output;
}
- private int calcSampleSize(Uri image, int size)
- throws IOException, SecurityException {
+ private int calcSampleSize(final Uri image, int size) throws IOException, SecurityException {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
- final InputStream inputStream = openInputStream(image);
- BitmapFactory.decodeStream(inputStream, null, options);
- close(inputStream);
- return calcSampleSize(options, size);
+ try (final InputStream inputStream =
+ openInputStream(image)) {
+ BitmapFactory.decodeStream(inputStream, null, options);
+ return calcSampleSize(options, size);
+ }
}
public void updateFileParams(final Message message) {
diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java
index c9be19551e3c456bbb403396d0ce3dcec1c9708e..1675ea718853af9bca8ffb912c5f123fd0da47a1 100644
--- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java
+++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java
@@ -33,26 +33,20 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
-import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import android.util.SparseArray;
-
import androidx.appcompat.app.AppCompatActivity;
-
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
-
+import de.gultsch.common.TrustManagers;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.BundledTrustManager;
-import eu.siacs.conversations.crypto.CombiningTrustManager;
-import eu.siacs.conversations.crypto.TrustManagers;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.entities.MTMDecision;
import eu.siacs.conversations.http.HttpConnectionManager;
@@ -89,10 +83,12 @@ import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
-
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
/**
* A X509 trust manager implementation which asks the user about invalid certificates and memorizes
@@ -118,7 +114,8 @@ public class MemorizingTrustManager {
"\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED =
Pattern.compile(
- "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
+ "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
+ + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
private static final Pattern PATTERN_IPV6_6HEX4DEC =
Pattern.compile(
"\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
@@ -180,11 +177,7 @@ public class MemorizingTrustManager {
this.appTrustManager = getTrustManager(appKeyStore);
this.daneVerifier = new DaneVerifier();
try {
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
- this.defaultTrustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
- } else {
- this.defaultTrustManager = TrustManagers.createDefaultTrustManager();
- }
+ this.defaultTrustManager = TrustManagers.createForAndroidVersion(context);
} catch (final NoSuchAlgorithmException
| KeyStoreException
| CertificateException
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index bccd71abe82612df0a30bb1d525a2933cac69e1b..b01e2ab9295cea4c9f39c11d480ed1aa8637f0ca 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -4183,7 +4183,7 @@ public class XmppConnectionService extends Service {
}
private void joinMuc(
- Conversation conversation,
+ final Conversation conversation,
final OnConferenceJoined onConferenceJoined,
final boolean followedInvite) {
final Account account = conversation.getAccount();
@@ -4933,6 +4933,7 @@ public class XmppConnectionService extends Service {
bookmark == null ? null : bookmark.getBookmarkName(),
mucOptions.getName());
+ final var hadOccupantId = mucOptions.occupantId();
if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
Log.d(
Config.LOGTAG,
@@ -4942,6 +4943,25 @@ public class XmppConnectionService extends Service {
updateConversation(conversation);
}
+ final var hasOccupantId = mucOptions.occupantId();
+
+ if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
+ final var me = mucOptions.getSelf().getFullJid();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": gained support for occupant-id in "
+ + me
+ + ". resending presence");
+ final var packet =
+ mPresenceGenerator.selfPresence(
+ account,
+ Presence.Status.ONLINE,
+ mucOptions.nonanonymous(), mucOptions.getSelf().getNick());
+ packet.setTo(me);
+ sendPresencePacket(account, packet);
+ }
+
if (bookmark != null
&& (sameBefore || bookmark.getBookmarkName() == null)) {
if (bookmark.setBookmarkName(
@@ -6401,13 +6421,13 @@ public class XmppConnectionService extends Service {
if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
final var mucOptions = conversation.getMucOptions();
if (!mucOptions.participating()) {
- Log.d(Config.LOGTAG, "not participating in MUC");
+ Log.e(Config.LOGTAG, "not participating in MUC");
return false;
}
final var self = mucOptions.getSelf();
final String occupantId = self.getOccupantId();
if (Strings.isNullOrEmpty(occupantId)) {
- Log.d(Config.LOGTAG, "occupant id not found for reaction in MUC");
+ Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
return false;
}
final var existingRaw =
@@ -6453,6 +6473,7 @@ public class XmppConnectionService extends Service {
null);
}
if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
+ Log.e(Config.LOGTAG, "could not find id to react to");
return false;
}
@@ -7102,10 +7123,6 @@ public class XmppConnectionService extends Service {
return verifiedSomething;
}
- public boolean blindTrustBeforeVerification() {
- return getBooleanPreference(AppSettings.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
- }
-
public ShortcutService getShortcutService() {
return mShortcutService;
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java
index ff60e6419b2abde89bbc53b6fce4417bb90bef7b..767109a36675111e2a89460d0a05736eab715c80 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java
@@ -24,7 +24,7 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity {
}
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityManageAccountsBinding binding =
DataBindingUtil.setContentView(this, R.layout.activity_manage_accounts);
@@ -64,11 +64,12 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity {
}
}
- private void goToProfilePictureActivity(Account account) {
+ private void goToProfilePictureActivity(final Account account) {
final Intent startIntent = getIntent();
final Uri uri = startIntent == null ? null : startIntent.getData();
if (uri != null) {
Intent intent = new Intent(this, PublishProfilePictureActivity.class);
+ intent.setAction(Intent.ACTION_ATTACH_DATA);
intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index 4c731d9d97b832b977bdbc812316a5eae8a01f84..d2ef50c6d82a6ea823975492aa2afeb925431645 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -49,6 +49,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import eu.siacs.conversations.Config;
+import de.gultsch.common.Linkify;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
import eu.siacs.conversations.databinding.ThreadRowBinding;
@@ -65,13 +66,13 @@ import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.ui.adapter.MediaAdapter;
import eu.siacs.conversations.ui.adapter.UserPreviewAdapter;
import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
+import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.GridManager;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.MucConfiguration;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
-import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
@@ -679,7 +680,8 @@ public class ConferenceDetailsActivity extends XmppActivity
if (printableValue(subject)) {
SpannableStringBuilder spannable = new SpannableStringBuilder(subject);
StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
- MyLinkify.addLinks(spannable, false);
+ Linkify.addLinks(spannable);
+ FixedURLSpan.fix(spannable);
this.binding.mucSubject.setText(spannable);
this.binding.mucSubject.setTextAppearance(
subject.length() > (hasTitle ? 128 : 196)
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 1079e5000e08df2cbd2f3e5545e6110f349e35fa..994dd65c7863ff91c086a79d0effa161fe63963a 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -39,9 +39,9 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.text.Editable;
-import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.SpannableStringBuilder;
import android.text.style.ImageSpan;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -132,6 +132,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import com.google.common.collect.Iterables;
+import de.gultsch.common.Linkify;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -997,7 +1000,7 @@ public class ConversationFragment extends XmppFragment
message = new Message(conversation, body.toString(), conversation.getNextEncryption());
message.setBody(hasSubject && body.length() == 0 ? null : body);
if (message.bodyIsOnlyEmojis()) {
- SpannableStringBuilder spannable = message.getSpannableBody(null, null);
+ var spannable = message.getSpannableBody(null, null);
ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
for (ImageSpan span : imageSpans) {
final int start = spannable.getSpanStart(span);
@@ -1758,7 +1761,7 @@ public class ConversationFragment extends XmppFragment
return;
}
- SpannableStringBuilder body = message.getSpannableBody(null, null);
+ var body = message.getSpannableBody(null, null);
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
messageListAdapter.handleTextQuotes(binding.contextPreviewText, body);
binding.contextPreviewText.setText(body);
@@ -1914,13 +1917,22 @@ public class ConversationFragment extends XmppFragment
&& t == null) {
copyMessage.setVisible(true);
quoteMessage.setVisible(!showError && !MessageUtils.prepareQuote(m).isEmpty());
- final String scheme =
- ShareUtil.getLinkScheme(new SpannableStringBuilder(m.getBody()));
- if ("xmpp".equals(scheme)) {
- copyLink.setTitle(R.string.copy_jabber_id);
- copyLink.setVisible(true);
- } else if (scheme != null) {
+ final var firstUri = Iterables.getFirst(Linkify.getLinks(m.getBody()), null);
+ if (firstUri != null) {
+ final var scheme = firstUri.getScheme();
+ final @StringRes int resForScheme =
+ switch (scheme) {
+ case "xmpp" -> R.string.copy_jabber_id;
+ case "http", "https", "gemini" -> R.string.copy_link;
+ case "geo" -> R.string.copy_geo_uri;
+ case "tel" -> R.string.copy_telephone_number;
+ case "mailto" -> R.string.copy_email_address;
+ default -> R.string.copy_URI;
+ };
+ copyLink.setTitle(resForScheme);
copyLink.setVisible(true);
+ } else {
+ copyLink.setVisible(false);
}
}
quoteMessage.setVisible(!encrypted && !showError);
@@ -3008,12 +3020,14 @@ public class ConversationFragment extends XmppFragment
builder.setNegativeButton(
R.string.copy_to_clipboard,
(dialog, which) -> {
- activity.copyTextToClipboard(displayError, R.string.error_message);
- Toast.makeText(
- activity,
- R.string.error_message_copied_to_clipboard,
- Toast.LENGTH_SHORT)
- .show();
+ if (activity.copyTextToClipboard(displayError, R.string.error_message)
+ && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Toast.makeText(
+ activity,
+ R.string.error_message_copied_to_clipboard,
+ Toast.LENGTH_SHORT)
+ .show();
+ }
});
builder.setPositiveButton(R.string.confirm, null);
builder.create().show();
@@ -3633,7 +3647,7 @@ public class ConversationFragment extends XmppFragment
}
}
} else {
- if (text != null && GeoHelper.GEO_URI.matcher(text).matches()) {
+ if (text != null && Patterns.URI_GEO.matcher(text).matches()) {
mediaPreviewAdapter.addMediaPreviews(
Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION));
toggleInputMethod();
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index 3c526cfe3f1fccf05cee2935751ec4e374c411b1..63741f007bf1cdc5209eb03cac3b9a3a7a4db4d4 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -1329,6 +1329,10 @@ public class EditAccountActivity extends OmemoActivity
this.binding.accountPassword.setFocusableInTouchMode(editPassword);
this.binding.accountPassword.setCursorVisible(editPassword);
this.binding.accountPassword.setEnabled(editPassword);
+ if (!editPassword && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ this.binding.accountJid.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
+ this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
+ }
if (!mInitMode) {
this.binding.avater.setVisibility(View.VISIBLE);
@@ -1463,6 +1467,13 @@ public class EditAccountActivity extends OmemoActivity
this.mAccount.getAxolotlService().getOwnFingerprint();
if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
+ this.binding.axolotlFingerprintBox.setOnCreateContextMenuListener(
+ (menu, v, menuInfo) -> {
+ getMenuInflater().inflate(R.menu.omemo_key_context, menu);
+ menu.findItem(R.id.verify_scan).setVisible(false);
+ menu.findItem(R.id.distrust_key).setVisible(false);
+ this.mSelectedFingerprint = ownAxolotlFingerprint;
+ });
if (ownAxolotlFingerprint.equals(messageFingerprint)) {
this.binding.ownFingerprintDesc.setTextColor(
MaterialColors.getColor(
diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java
index 7ed58127c0eb7700e64fa0598c833f5f19efdd34..c78cf4dbc6be4972fb5ffabf3def9c8cf136158d 100644
--- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java
@@ -1,12 +1,14 @@
package eu.siacs.conversations.ui;
import android.content.Intent;
+import android.os.Build;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -22,7 +24,7 @@ import eu.siacs.conversations.utils.XmppUri;
public abstract class OmemoActivity extends XmppActivity {
private Account mSelectedAccount;
- private String mSelectedFingerprint;
+ protected String mSelectedFingerprint;
protected XmppUri mPendingFingerprintVerificationUri = null;
@@ -50,25 +52,27 @@ public abstract class OmemoActivity extends XmppActivity {
distrust.setVisible(
status.isVerified() || (!status.isActive() && status.isTrusted()));
}
+ // TODO can we rework this into using Intents?
this.mSelectedAccount = (Account) account;
this.mSelectedFingerprint = (String) fingerprint;
}
}
@Override
- public boolean onContextItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.distrust_key:
- showPurgeKeyDialog(mSelectedAccount, mSelectedFingerprint);
- break;
- case R.id.copy_omemo_key:
- copyOmemoFingerprint(mSelectedFingerprint);
- break;
- case R.id.verify_scan:
- ScanActivity.scan(this);
- break;
+ public boolean onContextItemSelected(final MenuItem item) {
+ final var itemId = item.getItemId();
+ if (itemId == R.id.distrust_key) {
+ showPurgeKeyDialog(mSelectedAccount, mSelectedFingerprint);
+ return true;
+ } else if (itemId == R.id.copy_omemo_key) {
+ copyOmemoFingerprint(mSelectedFingerprint);
+ return true;
+ } else if (itemId == R.id.verify_scan) {
+ ScanActivity.scan(this);
+ return true;
+ } else {
+ return super.onContextItemSelected(item);
}
- return true;
}
@Override
@@ -89,8 +93,9 @@ public abstract class OmemoActivity extends XmppActivity {
protected void copyOmemoFingerprint(String fingerprint) {
if (copyTextToClipboard(
- CryptoHelper.prettifyFingerprint(fingerprint.substring(2)),
- R.string.omemo_fingerprint)) {
+ CryptoHelper.prettifyFingerprint(fingerprint.substring(2)),
+ R.string.omemo_fingerprint)
+ && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Toast.makeText(this, R.string.toast_message_omemo_fingerprint, Toast.LENGTH_SHORT)
.show();
}
@@ -240,7 +245,9 @@ public abstract class OmemoActivity extends XmppActivity {
@Override
public void onRequestPermissionsResult(
- int requestCode, String[] permissions, int[] grantResults) {
+ final int requestCode,
+ @NonNull final String[] permissions,
+ @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java
index d0713219355b835c5095e3333073d5baeda28800..e363e8e8b8f44f98d513425aae2d24a4fb714470 100644
--- a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java
@@ -29,9 +29,6 @@
package eu.siacs.conversations.ui;
-import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE;
-
-import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.BitmapDrawable;
@@ -42,9 +39,11 @@ import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
+import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.StringRes;
import androidx.databinding.DataBindingUtil;
-import com.canhub.cropper.CropImage;
+import com.canhub.cropper.CropImageContract;
+import com.canhub.cropper.CropImageContractOptions;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
@@ -60,6 +59,15 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity
private Conversation conversation;
private Uri uri;
+ final ActivityResultLauncher cropImage =
+ registerForActivityResult(
+ new CropImageContract(),
+ cropResult -> {
+ if (cropResult.isSuccessful()) {
+ onAvatarPicked(cropResult.getUriContent());
+ }
+ });
+
@Override
protected void refreshUiReal() {}
@@ -103,8 +111,8 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity
configureActionBar(getSupportActionBar());
this.binding.cancelButton.setOnClickListener((v) -> this.finish());
this.binding.secondaryHint.setVisibility(View.GONE);
- this.binding.accountImage.setOnClickListener(
- (v) -> PublishProfilePictureActivity.chooseAvatar(this));
+ this.binding.accountImage.setOnClickListener((v) -> pickAvatar());
+
final var intent = getIntent();
final var uuid = intent == null ? null : intent.getStringExtra("uuid");
if (uuid != null) {
@@ -120,42 +128,17 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity
xmppConnectionService.publishMucAvatar(conversation, uri, this);
}
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
- final CropImage.ActivityResult result = CropImage.getActivityResult(data);
- if (resultCode == RESULT_OK) {
- this.uri = result == null ? null : result.getUri();
- if (xmppConnectionServiceBound) {
- reloadAvatar();
- }
- } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
- final var error = result == null ? null : result.getError();
- if (error != null) {
- Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
- }
- }
- } else if (requestCode == REQUEST_CHOOSE_PICTURE) {
- if (resultCode == RESULT_OK) {
- cropUri(data.getData());
- }
- }
+ public void pickAvatar() {
+ this.cropImage.launch(
+ new CropImageContractOptions(
+ null, PublishProfilePictureActivity.getCropImageOptions()));
}
- public void cropUri(final Uri uri) {
- if (Build.VERSION.SDK_INT >= 28) {
- this.uri = uri;
+ private void onAvatarPicked(final Uri uri) {
+ this.uri = uri;
+ if (xmppConnectionServiceBound) {
reloadAvatar();
- if (this.binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || this.binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) {
- return;
- }
}
-
- CropImage.activity(uri).setOutputCompressFormat(Bitmap.CompressFormat.PNG)
- .setAspectRatio(1, 1)
- .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
- .start(this);
}
@Override
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
index 1c3a33ab2ff11890efc868ab4e0e52463f423a81..1549ddbef90e866ff30bdaffec9720806ed4093f 100644
--- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -1,6 +1,5 @@
package eu.siacs.conversations.ui;
-import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.BitmapDrawable;
@@ -15,10 +14,13 @@ import android.view.MenuItem;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.widget.Toast;
+import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.databinding.DataBindingUtil;
-import com.canhub.cropper.CropImage;
+import com.canhub.cropper.CropImageContract;
+import com.canhub.cropper.CropImageContractOptions;
+import com.canhub.cropper.CropImageOptions;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -28,7 +30,6 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.utils.PhoneHelper;
-import java.util.concurrent.atomic.AtomicBoolean;
public class PublishProfilePictureActivity extends XmppActivity
implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
@@ -41,7 +42,6 @@ public class PublishProfilePictureActivity extends XmppActivity
private Account account;
private boolean support = false;
private boolean publishing = false;
- private final AtomicBoolean handledExternalUri = new AtomicBoolean(false);
private final OnLongClickListener backToDefaultListener =
new OnLongClickListener() {
@@ -54,6 +54,15 @@ public class PublishProfilePictureActivity extends XmppActivity
};
private boolean mInitialAccountSetup;
+ final ActivityResultLauncher cropImage =
+ registerForActivityResult(
+ new CropImageContract(),
+ cropResult -> {
+ if (cropResult.isSuccessful()) {
+ onAvatarPicked(cropResult.getUriContent());
+ }
+ });
+
@Override
public void onAvatarPublicationSucceeded() {
runOnUiThread(
@@ -89,6 +98,7 @@ public class PublishProfilePictureActivity extends XmppActivity
@Override
public void onCreate(final Bundle savedInstanceState) {
+
super.onCreate(savedInstanceState);
this.binding =
@@ -124,12 +134,10 @@ public class PublishProfilePictureActivity extends XmppActivity
}
finish();
});
- this.binding.accountImage.setOnClickListener(v -> chooseAvatar(this));
+ this.binding.accountImage.setOnClickListener(v -> pickAvatar(null));
this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
if (savedInstanceState != null) {
this.avatarUri = savedInstanceState.getParcelable("uri");
- this.handledExternalUri.set(
- savedInstanceState.getBoolean("handle_external_uri", false));
}
}
@@ -172,47 +180,32 @@ public class PublishProfilePictureActivity extends XmppActivity
if (this.avatarUri != null) {
outState.putParcelable("uri", this.avatarUri);
}
- outState.putBoolean("handle_external_uri", handledExternalUri.get());
super.onSaveInstanceState(outState);
}
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
- CropImage.ActivityResult result = CropImage.getActivityResult(data);
- if (resultCode == RESULT_OK) {
- this.avatarUri = result.getUri();
- if (xmppConnectionServiceBound) {
- loadImageIntoPreview(this.avatarUri);
- }
- } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
- Exception error = result.getError();
- if (error != null) {
- Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
- }
- }
- } else if (requestCode == REQUEST_CHOOSE_PICTURE) {
- if (resultCode == RESULT_OK) {
- cropUri(this, data.getData());
- }
- }
+ public void pickAvatar(final Uri image) {
+ this.cropImage.launch(new CropImageContractOptions(image, getCropImageOptions()));
}
- public static void chooseAvatar(final Activity activity) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.setType("image/*");
- activity.startActivityForResult(
- Intent.createChooser(
- intent, activity.getString(R.string.attach_choose_picture)),
- REQUEST_CHOOSE_PICTURE);
+ public static CropImageOptions getCropImageOptions() {
+ final var cropImageOptions = new CropImageOptions();
+ cropImageOptions.aspectRatioX = 1;
+ cropImageOptions.aspectRatioY = 1;
+ cropImageOptions.fixAspectRatio = true;
+ cropImageOptions.outputCompressFormat = Bitmap.CompressFormat.PNG;
+ cropImageOptions.imageSourceIncludeCamera = false;
+ cropImageOptions.minCropResultHeight = Config.AVATAR_SIZE;
+ cropImageOptions.minCropResultWidth = Config.AVATAR_SIZE;
+ return cropImageOptions;
+ }
+
+ private void onAvatarPicked(final Uri uri) {
+ Log.d(Config.LOGTAG, "onAvatarPicked(" + uri + ")");
+ this.avatarUri = uri;
+ if (xmppConnectionServiceBound) {
+ loadImageIntoPreview(uri);
} else {
- CropImage.activity()
- .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
- .setAspectRatio(1, 1)
- .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
- .start(activity);
+ Log.d(Config.LOGTAG, "not ready during avatarPick");
}
}
@@ -244,40 +237,30 @@ public class PublishProfilePictureActivity extends XmppActivity
public void onStart() {
super.onStart();
final Intent intent = getIntent();
- this.mInitialAccountSetup = intent != null && intent.getBooleanExtra("setup", false);
-
- final Uri uri = intent != null ? intent.getData() : null;
-
- if (uri != null && handledExternalUri.compareAndSet(false, true)) {
- cropUri(this, uri);
+ if (intent == null) {
+ return;
+ }
+ this.mInitialAccountSetup = intent.getBooleanExtra("setup", false);
+
+ final var data = intent.getData();
+ final var account = intent.getStringExtra(EXTRA_ACCOUNT);
+ if (Intent.ACTION_ATTACH_DATA.equals(intent.getAction())
+ && data != null
+ && account != null) {
+ pickAvatar(data);
+ final var replacement = new Intent(Intent.ACTION_MAIN);
+ replacement.putExtra(EXTRA_ACCOUNT, account);
+ setIntent(replacement);
return;
}
if (this.mInitialAccountSetup) {
this.binding.cancelButton.setText(R.string.skip);
}
- configureActionBar(
- getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get());
- }
-
- public void cropUri(final Activity activity, final Uri uri) {
- if (Build.VERSION.SDK_INT >= 28) {
- loadImageIntoPreview(uri);
- if (binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) {
- this.avatarUri = uri;
- return;
- }
- }
-
- CropImage.activity(uri)
- .setOutputCompressFormat(Bitmap.CompressFormat.PNG)
- .setAspectRatio(1, 1)
- .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
- .start(this);
+ configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup);
}
protected void loadImageIntoPreview(final Uri uri) {
-
Drawable bm = null;
if (uri == null) {
bm =
diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
index f0545f8ed567d025a01bba4d37c3458cce06a0c4..d97ad0168451c403840af9687a107e8e39458920 100644
--- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
@@ -1234,6 +1234,7 @@ public class RtpSessionActivity extends XmppActivity
}
},
MainThreadExecutor.getInstance());
+ // TODO ^ replace with ContextCompat.getMainExecutor(getApplication())
}
private void enableVideo(final View view) {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
index 24d268b3ca6baceaac69121f56e53243073dc307..591f65cee4b9597f6a0ffdd553873416973db360 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
@@ -61,7 +61,7 @@ public class AccountAdapter extends ArrayAdapter {
viewHolder.binding.accountStatus.setTextColor(
MaterialColors.getColor(
viewHolder.binding.accountStatus,
- com.google.android.material.R.attr.colorPrimary));
+ androidx.appcompat.R.attr.colorPrimary));
break;
case DISABLED:
case LOGGED_OUT:
@@ -75,7 +75,7 @@ public class AccountAdapter extends ArrayAdapter {
viewHolder.binding.accountStatus.setTextColor(
MaterialColors.getColor(
viewHolder.binding.accountStatus,
- com.google.android.material.R.attr.colorError));
+ androidx.appcompat.R.attr.colorError));
break;
}
if (account.isOnlineAndConnected()) {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
index 62c33522037c5406fd3638089c2bb7b6d9162a8d..4e0cdb1646aa21d47def814cf33a4f44e58fc467 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -7,17 +7,14 @@ import android.view.MenuItem;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import androidx.core.widget.ImageViewCompat;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
-
import com.google.android.material.color.MaterialColors;
import com.google.common.base.Optional;
-
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ItemConversationBinding;
import eu.siacs.conversations.entities.Conversation;
@@ -31,7 +28,6 @@ import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
-
import java.util.List;
public class ConversationAdapter
@@ -109,14 +105,14 @@ public class ConversationAdapter
ColorStateList.valueOf(
MaterialColors.getColor(
viewHolder.binding.messageStatus,
- com.google.android.material.R.attr.colorPrimary)));
+ androidx.appcompat.R.attr.colorPrimary)));
} else {
ImageViewCompat.setImageTintList(
viewHolder.binding.messageStatus,
ColorStateList.valueOf(
MaterialColors.getColor(
viewHolder.binding.messageStatus,
- com.google.android.material.R.attr.colorControlNormal)));
+ androidx.appcompat.R.attr.colorControlNormal)));
}
viewHolder.binding.messageStatus.setVisibility(View.VISIBLE);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java
index e88ba5d3da16de7d4ef65334135d531295d39278..91048df4e3e03efa1bce05a77f2ecddbf7103746 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java
@@ -18,11 +18,13 @@ import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.color.MaterialColors;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ItemMediaBinding;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.ui.util.ViewUtil;
+import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.worker.ExportBackupWorker;
import java.io.File;
@@ -35,13 +37,12 @@ import java.util.concurrent.RejectedExecutionException;
public class MediaAdapter extends RecyclerView.Adapter {
public static final List DOCUMENT_MIMES =
- Arrays.asList(
- "application/pdf",
- "application/vnd.oasis.opendocument.text",
- "application/msword",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- "text/x-tex",
- "text/plain");
+ new ImmutableList.Builder()
+ .add("application/pdf")
+ .add("text/x-tex")
+ .add("text/plain")
+ .addAll(MimeUtils.WORD_DOCUMENT_MIMES)
+ .build();
public static final List SPREAD_SHEET_MIMES =
Arrays.asList(
"text/comma-separated-values",
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
index 1365ea8f6f690f8dba90f69b2c4e248428deef8d..4af8119a8edd794669f001c62d7a73e9152ca57d 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -97,6 +97,7 @@ import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import net.fellbaum.jemoji.EmojiManager;
+import de.gultsch.common.Linkify;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -134,7 +135,6 @@ import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
-import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.ViewUtil;
@@ -335,8 +335,7 @@ public class MessageAdapter extends ArrayAdapter {
.time()
.setTextColor(
MaterialColors.getColor(
- viewHolder.time(),
- com.google.android.material.R.attr.colorError));
+ viewHolder.time(), androidx.appcompat.R.attr.colorError));
} else {
setTextColor(viewHolder.time(), bubbleColor);
}
@@ -638,7 +637,8 @@ public class MessageAdapter extends ArrayAdapter {
body.append("…");
}
if (processMarkup) StylingHelper.format(body, viewHolder.messageBody().getCurrentTextColor());
- MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
+ Linkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
+ FixedURLSpan.fix(body);
boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody(), body, bubbleColor, true) : false;
for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
int start = body.getSpanStart(quote);
@@ -1951,8 +1951,7 @@ public class MessageAdapter extends ArrayAdapter {
ImageViewCompat.setImageTintList(
imageView,
ColorStateList.valueOf(
- MaterialColors.getColor(
- imageView, com.google.android.material.R.attr.colorError)));
+ MaterialColors.getColor(imageView, androidx.appcompat.R.attr.colorError)));
}
public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
@@ -1960,8 +1959,7 @@ public class MessageAdapter extends ArrayAdapter {
textView.setTextColor(color);
if (BubbleColor.SURFACES.contains(bubbleColor)) {
textView.setLinkTextColor(
- MaterialColors.getColor(
- textView, com.google.android.material.R.attr.colorPrimary));
+ MaterialColors.getColor(textView, androidx.appcompat.R.attr.colorPrimary));
} else {
textView.setLinkTextColor(color);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java
index ec749405c359dbf5bba21ed4882ba0314d657fe1..2bdd76034543959380b62ade25a93f67b872d79d 100644
--- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java
@@ -1,12 +1,13 @@
package eu.siacs.conversations.ui.fragment.settings;
import android.Manifest;
+import android.content.Intent;
import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
-
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
@@ -23,17 +24,15 @@ import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
-
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.base.Strings;
import com.google.common.primitives.Longs;
-
+import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.worker.ExportBackupWorker;
-
import java.util.concurrent.TimeUnit;
public class BackupSettingsFragment extends XmppPreferenceFragment {
@@ -58,21 +57,34 @@ public class BackupSettingsFragment extends XmppPreferenceFragment {
}
});
+ private final ActivityResultLauncher pickBackupLocationLauncher =
+ registerForActivityResult(
+ new ActivityResultContracts.OpenDocumentTree(),
+ uri -> {
+ if (uri == null) {
+ Log.d(Config.LOGTAG, "no backup location selected");
+ return;
+ }
+ submitBackupLocationPreference(uri);
+ });
+
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.preferences_backup, rootKey);
final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP);
final var export = findPreference("export");
final ListPreference recurringBackup = findPreference(RECURRING_BACKUP);
- final var backupDirectory = findPreference("backup_directory");
- if (createOneOffBackup == null || recurringBackup == null || backupDirectory == null) {
+ final var backupLocation = findPreference(AppSettings.BACKUP_LOCATION);
+ if (createOneOffBackup == null || recurringBackup == null || backupLocation == null) {
throw new IllegalStateException(
"The preference resource file is missing some preferences");
}
- backupDirectory.setSummary(
+ final var appSettings = new AppSettings(requireContext());
+ backupLocation.setSummary(
getString(
R.string.pref_create_backup_summary,
- FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
+ appSettings.getBackupLocationAsPath()));
+ backupLocation.setOnPreferenceClickListener(this::onBackupLocationPreferenceClicked);
createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
export.setOnPreferenceClickListener(this::onExportClicked);
setValues(
@@ -81,6 +93,26 @@ public class BackupSettingsFragment extends XmppPreferenceFragment {
value -> timeframeValueToName(requireContext(), value));
}
+ private boolean onBackupLocationPreferenceClicked(final Preference preference) {
+ this.pickBackupLocationLauncher.launch(null);
+ return false;
+ }
+
+ private void submitBackupLocationPreference(final Uri uri) {
+ final var contentResolver = requireContext().getContentResolver();
+ contentResolver.takePersistableUriPermission(
+ uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ final var appSettings = new AppSettings(requireContext());
+ appSettings.setBackupLocation(uri);
+ final var preference = findPreference(AppSettings.BACKUP_LOCATION);
+ if (preference == null) {
+ return;
+ }
+ preference.setSummary(
+ getString(R.string.pref_create_backup_summary, AppSettings.asPath(uri)));
+ }
+
@Override
protected void onSharedPreferenceChanged(@NonNull String key) {
super.onSharedPreferenceChanged(key);
diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java
index aef72d558cc70bbabf31747ca0733474d13ee088..f8364611f972ce1ffec52327cfc5886ba6582cd3 100644
--- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java
+++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java
@@ -34,10 +34,10 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.os.Build;
import android.text.Editable;
import android.text.Spanned;
import android.text.style.URLSpan;
+import android.util.Log;
import android.view.SoundEffectConstants;
import android.view.View;
import android.widget.Toast;
@@ -46,37 +46,41 @@ import com.cheogram.android.BrowserHelper;
import java.util.Arrays;
+import com.google.common.base.Joiner;
+import de.gultsch.common.MiniUri;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.ShowLocationActivity;
+import java.util.Arrays;
@SuppressLint("ParcelCreator")
public class FixedURLSpan extends URLSpan {
protected final Account account;
- public FixedURLSpan(String url) {
+ public FixedURLSpan(final String url) {
this(url, null);
}
- public FixedURLSpan(String url, Account account) {
+ public FixedURLSpan(final String url, Account account) {
super(url);
this.account = account;
}
- public static void fix(final Editable editable) {
- for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
- final int start = editable.getSpanStart(urlspan);
- final int end = editable.getSpanEnd(urlspan);
- editable.removeSpan(urlspan);
- editable.setSpan(
- new FixedURLSpan(urlspan.getURL()),
- start,
- end,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- }
+ public static void fix(final Editable editable) {
+ for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
+ final int start = editable.getSpanStart(urlspan);
+ final int end = editable.getSpanEnd(urlspan);
+ editable.removeSpan(urlspan);
+ editable.setSpan(
+ new FixedURLSpan(urlspan.getURL()),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
@Override
public void onClick(View widget) {
@@ -107,7 +111,19 @@ public class FixedURLSpan extends URLSpan {
return;
}
- final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ var intent = new Intent(Intent.ACTION_VIEW, uri);
+ if ("web+ap".equals(uri.getScheme())) {
+ if (intent.resolveActivity(context.getPackageManager()) == null) {
+ Log.d(Config.LOGTAG, "no app found to handle web+ap");
+ final var webApAsHttps =
+ Uri.parse(
+ String.format(
+ "https://%s/%s",
+ uri.getAuthority(),
+ Joiner.on('/').join(uri.getPathSegments())));
+ intent = new Intent(Intent.ACTION_VIEW, webApAsHttps);
+ }
+ }
if ("geo".equalsIgnoreCase(uri.getScheme())) {
intent.setClass(context, ShowLocationActivity.class);
} else {
@@ -118,10 +134,10 @@ public class FixedURLSpan extends URLSpan {
widget.playSoundEffect(SoundEffectConstants.CLICK);
} catch (ActivityNotFoundException e) {
if ("bitcoin".equals(uri.getScheme()) || "bitcoincash".equals(uri.getScheme()) || "monero".equals(uri.getScheme())) {
- Toast.makeText(context, "No compatible wallet app found", Toast.LENGTH_SHORT).show();
+ Toast.makeText(context, "No compatible wallet app found", Toast.LENGTH_SHORT).show();
} else {
- Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
- }
+ Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
+ }
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java
index 8c13a2e7e2b939d6fff30fdcd9ea9ed8e3135bad..9fd11cb426d0e69911379b8db2f6f0fe83ee50dc 100644
--- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java
+++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java
@@ -41,18 +41,28 @@ import android.widget.Toast;
import java.util.regex.Matcher;
+import android.os.Build;
+import android.widget.Toast;
+import androidx.annotation.StringRes;
+import com.google.common.collect.Iterables;
+import de.gultsch.common.Linkify;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.XmppActivity;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
+import java.util.Arrays;
+import java.util.Collection;
public class ShareUtil {
+ private static final Collection SCHEMES_COPY_PATH_ONLY =
+ Arrays.asList("xmpp", "mailto", "tel");
+
+
public static void share(XmppActivity activity, Message message) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
@@ -101,14 +111,26 @@ public class ShareUtil {
}
}
- public static void copyToClipboard(XmppActivity activity, Message message) {
- if (activity.copyTextToClipboard(message.getQuoteableBody(), R.string.message)) {
+ public static void copyToClipboard(final XmppActivity activity, final Message message) {
+ if (activity.copyTextToClipboard(message.getQuoteableBody(), R.string.message)
+ && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT)
.show();
}
}
- public static void copyUrlToClipboard(XmppActivity activity, Message message) {
+ public static boolean copyTextToClipboard(Context context, String text, int labelResId) {
+ ClipboardManager mClipBoardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ String label = context.getResources().getString(labelResId);
+ if (mClipBoardManager != null) {
+ ClipData mClipData = ClipData.newPlainText(label, text);
+ mClipBoardManager.setPrimaryClip(mClipData);
+ return true;
+ }
+ return false;
+ }
+
+ public static void copyUrlToClipboard(final XmppActivity activity, final Message message) {
final String url;
final int resId;
if (message.isGeoUri()) {
@@ -125,11 +147,13 @@ public class ShareUtil {
: message.getRawBody().trim();
resId = R.string.file_url;
}
- if (activity.copyTextToClipboard(url, resId)) {
+ if (activity.copyTextToClipboard(url, resId)
+ && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
+
public static void copyLinkToClipboard(final Context context, final String url) {
final Uri uri = Uri.parse(url);
if ("xmpp".equals(uri.getScheme())) {
@@ -147,35 +171,33 @@ public class ShareUtil {
}
public static void copyLinkToClipboard(final XmppActivity activity, final Message message) {
- final SpannableStringBuilder body = message.getSpannableBody();
- MyLinkify.addLinks(body, true);
- for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) {
- copyLinkToClipboard(activity, urlspan.getURL());
+ final var firstUri = Iterables.getFirst(Linkify.getLinks(message.getBody()), null);
+ if (firstUri == null) {
return;
}
- }
-
- public static boolean copyTextToClipboard(Context context, String text, int labelResId) {
- ClipboardManager mClipBoardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
- String label = context.getResources().getString(labelResId);
- if (mClipBoardManager != null) {
- ClipData mClipData = ClipData.newPlainText(label, text);
- mClipBoardManager.setPrimaryClip(mClipData);
- return true;
+ final String clip;
+ if (SCHEMES_COPY_PATH_ONLY.contains(firstUri.getScheme())) {
+ clip = firstUri.getPath();
+ } else {
+ clip = firstUri.getRaw();
}
- return false;
- }
-
- public static String getLinkScheme(final SpannableStringBuilder body) {
- MyLinkify.addLinks(body, false);
- for (final String url : MyLinkify.extractLinks(body)) {
- final Uri uri = Uri.parse(url);
- if ("xmpp".equals(uri.getScheme())) {
- return uri.getScheme();
- } else {
- return "http";
- }
+ final @StringRes int label =
+ switch (firstUri.getScheme()) {
+ case "http", "https", "gemini" -> R.string.web_address;
+ case "xmpp" -> R.string.account_settings_jabber_id;
+ default -> R.string.uri;
+ };
+ final @StringRes int toast =
+ switch (firstUri.getScheme()) {
+ case "http", "https", "gemini", "web+ap" -> R.string.url_copied_to_clipboard;
+ case "xmpp" -> R.string.jabber_id_copied_to_clipboard;
+ case "tel" -> R.string.copied_phone_number;
+ case "mailto" -> R.string.copied_email_address;
+ default -> R.string.uri_copied_to_clipboard;
+ };
+ if (activity.copyTextToClipboard(clip, label)
+ && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Toast.makeText(activity, toast, Toast.LENGTH_SHORT).show();
}
- return null;
}
}
diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFile.java b/src/main/java/eu/siacs/conversations/utils/BackupFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..7644f6ccbf2f64f08140159e53ae9f1edd1ad34c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/BackupFile.java
@@ -0,0 +1,185 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Context;
+import android.content.UriPermission;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import androidx.documentfile.provider.DocumentFile;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.worker.ExportBackupWorker;
+import eu.siacs.conversations.xmpp.Jid;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class BackupFile implements Comparable {
+
+ private static final ExecutorService BACKUP_FILE_READER_EXECUTOR =
+ Executors.newSingleThreadExecutor();
+
+ private final Uri uri;
+ private final BackupFileHeader header;
+
+ private BackupFile(Uri uri, BackupFileHeader header) {
+ this.uri = uri;
+ this.header = header;
+ }
+
+ public static ListenableFuture readAsync(final Context context, final Uri uri) {
+ return Futures.submit(() -> read(context, uri), BACKUP_FILE_READER_EXECUTOR);
+ }
+
+ private static BackupFile read(final File file) throws IOException {
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ fileInputStream.close();
+ return new BackupFile(Uri.fromFile(file), backupFileHeader);
+ }
+
+ public static BackupFile read(final Context context, final Uri uri) throws IOException {
+ final InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ if (inputStream == null) {
+ throw new FileNotFoundException();
+ }
+ final DataInputStream dataInputStream = new DataInputStream(inputStream);
+ final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ inputStream.close();
+ return new BackupFile(uri, backupFileHeader);
+ }
+
+ public BackupFileHeader getHeader() {
+ return header;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public static ListenableFuture> listAsync(final Context context) {
+ return Futures.submit(() -> list(context), BACKUP_FILE_READER_EXECUTOR);
+ }
+
+ private static List list(final Context context) {
+ final var database = DatabaseBackend.getInstance(context);
+ final List accounts = database.getAccountJids(false);
+ final var backupFiles = new ImmutableList.Builder();
+ final var apps =
+ ImmutableSet.of("Conversations", "Quicksy", context.getString(R.string.app_name));
+
+ final var uriPermissions = context.getContentResolver().getPersistedUriPermissions();
+
+ for (final UriPermission uriPermission : uriPermissions) {
+ final var uri = uriPermission.getUri();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ && DocumentsContract.isTreeUri(uri)) {
+ Log.d(Config.LOGTAG, "looking for backups in " + uri);
+ final var tree = DocumentFile.fromTreeUri(context, uriPermission.getUri());
+ final var files = tree == null ? new DocumentFile[0] : tree.listFiles();
+ for (final DocumentFile documentFile : files) {
+ final var name = documentFile.getName();
+ if (documentFile.isFile()
+ && (ExportBackupWorker.MIME_TYPE.equals(documentFile.getType())
+ || (name != null && name.endsWith(".ceb")))) {
+ try {
+ final BackupFile backupFile =
+ BackupFile.read(context, documentFile.getUri());
+ if (accounts.contains(backupFile.getHeader().getJid())) {
+ Log.d(
+ Config.LOGTAG,
+ "skipping backup for " + backupFile.getHeader().getJid());
+ } else {
+ backupFiles.add(backupFile);
+ }
+ } catch (final IOException
+ | IllegalArgumentException
+ | BackupFileHeader.OutdatedBackupFileVersion e) {
+ Log.d(Config.LOGTAG, "unable to read backup file ", e);
+ }
+ }
+ }
+ }
+ }
+
+ final List directories = new ArrayList<>();
+ for (final String app : apps) {
+ directories.add(FileBackend.getLegacyBackupDirectory(app));
+ }
+ if (uriPermissions.isEmpty()) {
+ Log.d(
+ Config.LOGTAG,
+ "including default directory since no uri permissions have been granted");
+ directories.add(FileBackend.getBackupDirectory(context));
+ }
+ for (final File directory : directories) {
+ if (!directory.exists() || !directory.isDirectory()) {
+ Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
+ continue;
+ }
+ final File[] files = directory.listFiles();
+ if (files == null) {
+ continue;
+ }
+ Log.d(Config.LOGTAG, "looking for backups in " + directory);
+ for (final File file : files) {
+ if (file.isFile() && file.getName().endsWith(".ceb")) {
+ try {
+ final BackupFile backupFile = BackupFile.read(file);
+ if (accounts.contains(backupFile.getHeader().getJid())) {
+ Log.d(
+ Config.LOGTAG,
+ "skipping backup for " + backupFile.getHeader().getJid());
+ } else {
+ backupFiles.add(backupFile);
+ }
+ } catch (final IOException
+ | IllegalArgumentException
+ | BackupFileHeader.OutdatedBackupFileVersion e) {
+ Log.d(Config.LOGTAG, "unable to read backup file ", e);
+ }
+ }
+ }
+ }
+ final var list = backupFiles.build();
+ if (QuickConversationsService.isQuicksy()) {
+ return Ordering.natural()
+ .immutableSortedCopy(
+ Collections2.filter(
+ list,
+ b ->
+ b.header
+ .getJid()
+ .getDomain()
+ .equals(Config.QUICKSY_DOMAIN)));
+ }
+ return Ordering.natural().immutableSortedCopy(backupFiles.build());
+ }
+
+ @Override
+ public int compareTo(final BackupFile o) {
+ return ComparisonChain.start()
+ .compare(header.getJid(), o.header.getJid())
+ .compare(o.header.getTimestamp(), header.getTimestamp())
+ .result();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
index a63c3e65dc0fa5e1e65bb9567d819e885b79537c..648ad049b5761592ba0e525a747edd916846d9b2 100644
--- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
@@ -5,148 +5,168 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
-
-import org.osmdroid.util.GeoPoint;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ShareLocationActivity;
import eu.siacs.conversations.ui.ShowLocationActivity;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import org.osmdroid.util.GeoPoint;
public class GeoHelper {
- private static final String SHARE_LOCATION_PACKAGE_NAME = "eu.siacs.conversations.location.request";
- private static final String SHOW_LOCATION_PACKAGE_NAME = "eu.siacs.conversations.location.show";
-
- public static Pattern GEO_URI = Pattern.compile("geo:(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)(?:,-?\\d+(?:\\.\\d+)?)?(?:;crs=[\\w-]+)?(?:;u=\\d+(?:\\.\\d+)?)?(?:;[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+)*(\\?z=\\d+)?", Pattern.CASE_INSENSITIVE);
-
- public static boolean isLocationPluginInstalled(Context context) {
- return new Intent(SHARE_LOCATION_PACKAGE_NAME).resolveActivity(context.getPackageManager()) != null;
- }
-
- public static boolean isLocationPluginInstalledAndDesired(Context context) {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- final boolean configured = preferences.getBoolean("use_share_location_plugin", context.getResources().getBoolean(R.bool.use_share_location_plugin));
- return configured && isLocationPluginInstalled(context);
- }
-
- public static Intent getFetchIntent(Context context) {
- if (isLocationPluginInstalledAndDesired(context)) {
- return new Intent(SHARE_LOCATION_PACKAGE_NAME);
- } else {
- return new Intent(context, ShareLocationActivity.class);
- }
- }
-
- public static GeoPoint parseGeoPoint(final Uri uri) {
- return parseGeoPoint(uri.toString());
- }
-
- private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException {
- final Matcher matcher = GEO_URI.matcher(body);
- if (!matcher.matches()) {
- throw new IllegalArgumentException("Invalid geo uri");
- }
- final double latitude;
- final double longitude;
- try {
- latitude = Double.parseDouble(matcher.group(1));
- if (latitude > 90.0 || latitude < -90.0) {
- throw new IllegalArgumentException("Invalid geo uri");
- }
- longitude = Double.parseDouble(matcher.group(2));
- if (longitude > 180.0 || longitude < -180.0) {
- throw new IllegalArgumentException("Invalid geo uri");
- }
- } catch (final NumberFormatException e) {
- throw new IllegalArgumentException("Invalid geo uri",e);
- }
- return new GeoPoint(latitude, longitude);
- }
-
- public static ArrayList createGeoIntentsFromMessage(Context context, Message message) {
- final ArrayList intents = new ArrayList<>();
- final GeoPoint geoPoint;
- try {
- geoPoint = parseGeoPoint(message.getRawBody());
- } catch (IllegalArgumentException e) {
- return intents;
- }
- final Conversational conversation = message.getConversation();
- final String label = getLabel(context, message);
-
- if (isLocationPluginInstalledAndDesired(context)) {
- Intent locationPluginIntent = new Intent(SHOW_LOCATION_PACKAGE_NAME);
- locationPluginIntent.putExtra("latitude", geoPoint.getLatitude());
- locationPluginIntent.putExtra("longitude", geoPoint.getLongitude());
- if (message.getStatus() != Message.STATUS_RECEIVED) {
- locationPluginIntent.putExtra("jid", conversation.getAccount().getJid().toString());
- locationPluginIntent.putExtra("name", conversation.getAccount().getJid().getLocal());
- } else {
- Contact contact = message.getContact();
- if (contact != null) {
- locationPluginIntent.putExtra("name", contact.getDisplayName());
- locationPluginIntent.putExtra("jid", contact.getJid().toString());
- } else {
- locationPluginIntent.putExtra("name", UIHelper.getDisplayedMucCounterpart(message.getCounterpart()));
- }
- }
- intents.add(locationPluginIntent);
- } else {
- Intent intent = new Intent(context, ShowLocationActivity.class);
- intent.setAction(SHOW_LOCATION_PACKAGE_NAME);
- intent.putExtra("latitude", geoPoint.getLatitude());
- intent.putExtra("longitude", geoPoint.getLongitude());
- intents.add(intent);
- }
-
- intents.add(geoIntent(geoPoint, label));
-
- Intent httpIntent = new Intent(Intent.ACTION_VIEW);
- httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+ geoPoint.getLatitude() + "," + geoPoint.getLongitude() +label));
- intents.add(httpIntent);
- return intents;
- }
-
- public static void view(Context context, Message message) {
- final GeoPoint geoPoint = parseGeoPoint(message.getRawBody());
- final String label = getLabel(context, message);
- context.startActivity(geoIntent(geoPoint,label));
- }
-
- private static Intent geoIntent(GeoPoint geoPoint, String label) {
- Intent geoIntent = new Intent(Intent.ACTION_VIEW);
- geoIntent.setData(Uri.parse("geo:" + geoPoint.getLatitude() + "," + geoPoint.getLongitude() + "?q=" + geoPoint.getLatitude() + "," + geoPoint.getLongitude() + "("+ label+")"));
- return geoIntent;
- }
-
- public static boolean openInOsmAnd(Context context, Message message) {
- try {
- final GeoPoint geoPoint = parseGeoPoint(message.getRawBody());
- final String label = getLabel(context, message);
- return geoIntent(geoPoint, label).resolveActivity(context.getPackageManager()) != null;
- } catch (IllegalArgumentException e) {
- return false;
- }
- }
-
- private static String getLabel(Context context, Message message) {
- if(message.getStatus() == Message.STATUS_RECEIVED) {
- try {
- return URLEncoder.encode(UIHelper.getMessageDisplayName(message),"UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError(e);
- }
- } else {
- return context.getString(R.string.me);
- }
- }
+ private static final String SHARE_LOCATION_PACKAGE_NAME =
+ "eu.siacs.conversations.location.request";
+ private static final String SHOW_LOCATION_PACKAGE_NAME = "eu.siacs.conversations.location.show";
+
+ public static boolean isLocationPluginInstalled(Context context) {
+ return new Intent(SHARE_LOCATION_PACKAGE_NAME).resolveActivity(context.getPackageManager())
+ != null;
+ }
+
+ public static boolean isLocationPluginInstalledAndDesired(Context context) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ final boolean configured =
+ preferences.getBoolean(
+ "use_share_location_plugin",
+ context.getResources().getBoolean(R.bool.use_share_location_plugin));
+ return configured && isLocationPluginInstalled(context);
+ }
+
+ public static Intent getFetchIntent(Context context) {
+ if (isLocationPluginInstalledAndDesired(context)) {
+ return new Intent(SHARE_LOCATION_PACKAGE_NAME);
+ } else {
+ return new Intent(context, ShareLocationActivity.class);
+ }
+ }
+
+ public static GeoPoint parseGeoPoint(final Uri uri) {
+ return parseGeoPoint(uri.toString());
+ }
+
+ private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException {
+ final Matcher matcher = Patterns.URI_GEO.matcher(body);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Invalid geo uri");
+ }
+ final double latitude;
+ final double longitude;
+ try {
+ latitude = Double.parseDouble(matcher.group(1));
+ if (latitude > 90.0 || latitude < -90.0) {
+ throw new IllegalArgumentException("Invalid geo uri");
+ }
+ longitude = Double.parseDouble(matcher.group(2));
+ if (longitude > 180.0 || longitude < -180.0) {
+ throw new IllegalArgumentException("Invalid geo uri");
+ }
+ } catch (final NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid geo uri", e);
+ }
+ return new GeoPoint(latitude, longitude);
+ }
+
+ public static ArrayList createGeoIntentsFromMessage(Context context, Message message) {
+ final ArrayList intents = new ArrayList<>();
+ final GeoPoint geoPoint;
+ try {
+ geoPoint = parseGeoPoint(message.getRawBody());
+ } catch (IllegalArgumentException e) {
+ return intents;
+ }
+ final Conversational conversation = message.getConversation();
+ final String label = getLabel(context, message);
+
+ if (isLocationPluginInstalledAndDesired(context)) {
+ Intent locationPluginIntent = new Intent(SHOW_LOCATION_PACKAGE_NAME);
+ locationPluginIntent.putExtra("latitude", geoPoint.getLatitude());
+ locationPluginIntent.putExtra("longitude", geoPoint.getLongitude());
+ if (message.getStatus() != Message.STATUS_RECEIVED) {
+ locationPluginIntent.putExtra("jid", conversation.getAccount().getJid().toString());
+ locationPluginIntent.putExtra(
+ "name", conversation.getAccount().getJid().getLocal());
+ } else {
+ Contact contact = message.getContact();
+ if (contact != null) {
+ locationPluginIntent.putExtra("name", contact.getDisplayName());
+ locationPluginIntent.putExtra("jid", contact.getJid().toString());
+ } else {
+ locationPluginIntent.putExtra(
+ "name", UIHelper.getDisplayedMucCounterpart(message.getCounterpart()));
+ }
+ }
+ intents.add(locationPluginIntent);
+ } else {
+ Intent intent = new Intent(context, ShowLocationActivity.class);
+ intent.setAction(SHOW_LOCATION_PACKAGE_NAME);
+ intent.putExtra("latitude", geoPoint.getLatitude());
+ intent.putExtra("longitude", geoPoint.getLongitude());
+ intents.add(intent);
+ }
+
+ intents.add(geoIntent(geoPoint, label));
+
+ Intent httpIntent = new Intent(Intent.ACTION_VIEW);
+ httpIntent.setData(
+ Uri.parse(
+ "https://maps.google.com/maps?q=loc:"
+ + geoPoint.getLatitude()
+ + ","
+ + geoPoint.getLongitude()
+ + label));
+ intents.add(httpIntent);
+ return intents;
+ }
+
+ public static void view(Context context, Message message) {
+ final GeoPoint geoPoint = parseGeoPoint(message.getRawBody());
+ final String label = getLabel(context, message);
+ context.startActivity(geoIntent(geoPoint, label));
+ }
+
+ private static Intent geoIntent(GeoPoint geoPoint, String label) {
+ Intent geoIntent = new Intent(Intent.ACTION_VIEW);
+ geoIntent.setData(
+ Uri.parse(
+ "geo:"
+ + geoPoint.getLatitude()
+ + ","
+ + geoPoint.getLongitude()
+ + "?q="
+ + geoPoint.getLatitude()
+ + ","
+ + geoPoint.getLongitude()
+ + "("
+ + label
+ + ")"));
+ return geoIntent;
+ }
+
+ public static boolean openInOsmAnd(Context context, Message message) {
+ try {
+ final GeoPoint geoPoint = parseGeoPoint(message.getRawBody());
+ final String label = getLabel(context, message);
+ return geoIntent(geoPoint, label).resolveActivity(context.getPackageManager()) != null;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ private static String getLabel(Context context, Message message) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ try {
+ return URLEncoder.encode(UIHelper.getMessageDisplayName(message), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ } else {
+ return context.getString(R.string.me);
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java
index 8b07c98f66eadfa8e259e6458303d4314c177ad5..b259ec3dcbb874ae1d9ea4d6f750bb9597ce3a5f 100644
--- a/src/main/java/eu/siacs/conversations/utils/IP.java
+++ b/src/main/java/eu/siacs/conversations/utils/IP.java
@@ -1,34 +1,18 @@
package eu.siacs.conversations.utils;
import com.google.common.net.InetAddresses;
+import de.gultsch.common.Patterns;
import java.net.InetAddress;
-import java.util.regex.Pattern;
public class IP {
- private static final Pattern PATTERN_IPV4 =
- Pattern.compile(
- "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
- private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED =
- Pattern.compile(
- "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
- + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
- private static final Pattern PATTERN_IPV6_6HEX4DEC =
- Pattern.compile(
- "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
- private static final Pattern PATTERN_IPV6_HEXCOMPRESSED =
- Pattern.compile(
- "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
- private static final Pattern PATTERN_IPV6 =
- Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
-
public static boolean matches(final String server) {
return server != null
- && (PATTERN_IPV4.matcher(server).matches()
- || PATTERN_IPV6.matcher(server).matches()
- || PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
- || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
- || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
+ && (Patterns.IPV4.matcher(server).matches()
+ || Patterns.IPV6.matcher(server).matches()
+ || Patterns.IPV6_6HEX4DEC.matcher(server).matches()
+ || Patterns.IPV6_HEX4_DECOMPRESSED.matcher(server).matches()
+ || Patterns.IPV6_HEX_COMPRESSED.matcher(server).matches());
}
public static String wrapIPv6(final String host) {
diff --git a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java
index 9dcdbb15e11cfb90154392a20409d814d61de376..748dd497d7e4468f4d45b953baead004f703c8f2 100644
--- a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java
+++ b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java
@@ -77,14 +77,12 @@ public class IrregularUnicodeDetector {
return style(
jid,
MaterialColors.getColor(
- context,
- com.google.android.material.R.attr.colorError,
- "colorError not found"));
+ context, androidx.appcompat.R.attr.colorError, "colorError not found"));
}
- private static Spannable style(Jid jid, @ColorInt int color) {
- PatternTuple patternTuple = find(jid);
- SpannableStringBuilder builder = new SpannableStringBuilder();
+ private static Spannable style(final Jid jid, final @ColorInt int color) {
+ final var patternTuple = find(jid);
+ final var builder = new SpannableStringBuilder();
if (jid.getLocal() != null && patternTuple.local != null) {
SpannableString local = new SpannableString(jid.getLocal());
colorize(local, patternTuple.local, color);
@@ -92,7 +90,7 @@ public class IrregularUnicodeDetector {
builder.append('@');
}
if (jid.getDomain() != null) {
- String[] labels = jid.getDomain().toString().split("\\.");
+ final var labels = jid.getDomain().toString().split("\\.");
for (int i = 0; i < labels.length; ++i) {
SpannableString spannableString = new SpannableString(labels[i]);
colorize(spannableString, patternTuple.domain.get(i), color);
diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
index d1d2140c0bbe87e0174cd79599973cb49529ce2b..4d3e34cc2b0475d4a8476b90ce229fba99bb9d77 100644
--- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
@@ -48,6 +48,12 @@ public final class MimeUtils {
"video/3gpp", // .3gp files can contain audio, video or both
"video/3gpp2");
+ public static final List WORD_DOCUMENT_MIMES =
+ Arrays.asList(
+ "application/vnd.oasis.opendocument.text",
+ "application/msword",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+
private static final Map mimeTypeToExtensionMap = new HashMap<>();
private static final Map extensionToMimeTypeMap = new HashMap<>();
diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java
deleted file mode 100644
index 9b4695b64014184ead70b0ddefb3c9a412ec1d17..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/utils/Patterns.java
+++ /dev/null
@@ -1,554 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- *
- * Download latest version here:
- * https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/util/Patterns.java
- *
- *
- */
-package eu.siacs.conversations.utils;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-/**
- * Commonly used regular expression patterns.
- */
-public class Patterns {
-
- public static final Pattern XMPP_PATTERN = Pattern
- .compile("xmpp\\:(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+");
-
- public static final Pattern BITCOIN_URI = Pattern
- .compile("bitcoin\\:(?:[13][a-km-zA-HJ-NP-Z1-9]{25,34}|[bB][cC]1[pPqQ][a-zA-Z0-9]{38,58})(?:\\?(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+)?");
-
- public static final Pattern BITCOINCASH_URI = Pattern
- .compile("bitcoincash\\:(?:[13][a-km-zA-HJ-NP-Z1-9]{33}|[qp][a-z0-9]{41})(?:\\?(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+)?");
-
- public static final Pattern ETHEREUM_URI = Pattern
- .compile("ethereum\\:(?:pay\\-)?(0x[0-9a-f]{40})(?:@[0-9]+)?(?:/(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+)?(?:\\?(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+)?");
-
- public static final Pattern MONERO_URI = Pattern
- .compile("monero\\:(?:[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93})(?:\\?(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+)?");
-
- public static final Pattern WOWNERO_URI = Pattern
- .compile("wownero\\:(?:W(?:[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{96}|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{106}|[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{187}))(?:\\?(?:(?:["
- + Patterns.GOOD_IRI_CHAR
- + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
- + "|(?:\\%[a-fA-F0-9]{2}))+)?");
-
- /**
- * Regular expression to match all IANA top-level domains.
- * List accurate as of 2011/07/18. List taken from:
- * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
- * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
- *
- * @deprecated Due to the recent profileration of gTLDs, this API is
- * expected to become out-of-date very quickly. Therefore it is now
- * deprecated.
- */
- @Deprecated
- public static final String TOP_LEVEL_DOMAIN_STR =
- "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
- + "|(biz|b[abdefghijmnorstvwyz])"
- + "|(cat|com|coop|c[acdfghiklmnoruvxyz])"
- + "|d[ejkmoz]"
- + "|(edu|e[cegrstu])"
- + "|f[ijkmor]"
- + "|(gov|g[abdefghilmnpqrstuwy])"
- + "|h[kmnrtu]"
- + "|(info|int|i[delmnoqrst])"
- + "|(jobs|j[emop])"
- + "|k[eghimnprwyz]"
- + "|l[abcikrstuvy]"
- + "|(mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
- + "|(name|net|n[acefgilopruz])"
- + "|(org|om)"
- + "|(pro|p[aefghklmnrstwy])"
- + "|qa"
- + "|r[eosuw]"
- + "|s[abcdeghijklmnortuvyz]"
- + "|(tel|travel|t[cdfghjklmnoprtvwz])"
- + "|u[agksyz]"
- + "|v[aceginu]"
- + "|w[fs]"
- + "|(\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
- + "|y[et]"
- + "|z[amw])";
- /**
- * Regular expression pattern to match all IANA top-level domains.
- * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
- */
- @Deprecated
- public static final Pattern TOP_LEVEL_DOMAIN =
- Pattern.compile(TOP_LEVEL_DOMAIN_STR);
- /**
- * Regular expression to match all IANA top-level domains for WEB_URL.
- * List accurate as of 2011/07/18. List taken from:
- * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
- * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
- *
- * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
- */
- @Deprecated
- public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
- "(?:"
- + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
- + "|(?:biz|b[abdefghijmnorstvwyz])"
- + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
- + "|d[ejkmoz]"
- + "|(?:edu|e[cegrstu])"
- + "|f[ijkmor]"
- + "|(?:gov|g[abdefghilmnpqrstuwy])"
- + "|h[kmnrtu]"
- + "|(?:info|int|i[delmnoqrst])"
- + "|(?:jobs|j[emop])"
- + "|k[eghimnprwyz]"
- + "|l[abcikrstuvy]"
- + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
- + "|(?:name|net|n[acefgilopruz])"
- + "|(?:org|om)"
- + "|(?:pro|p[aefghklmnrstwy])"
- + "|qa"
- + "|r[eosuw]"
- + "|s[abcdeghijklmnortuvyz]"
- + "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
- + "|u[agksyz]"
- + "|v[aceginu]"
- + "|w[fs]"
- + "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
- + "|y[et]"
- + "|z[amw]))";
- /**
- * Regular expression to match all IANA top-level domains.
- *
- * List accurate as of 2015/11/24. List taken from:
- * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
- * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
- *
- * @hide
- */
- static final String IANA_TOP_LEVEL_DOMAINS =
- "(?:"
- + "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active"
- + "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam"
- + "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates"
- + "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])"
- + "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva"
- + "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black"
- + "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique"
- + "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business"
- + "|buzz|bzh|b[abdefghijmnorstvwyz])"
- + "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards"
- + "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo"
- + "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco"
- + "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach"
- + "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos"
- + "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses"
- + "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])"
- + "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta"
- + "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount"
- + "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])"
- + "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises"
- + "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed"
- + "|express|e[cegrstu])"
- + "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film"
- + "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth"
- + "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi"
- + "|f[ijkmor])"
- + "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving"
- + "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger"
- + "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])"
- + "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings"
- + "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai"
- + "|h[kmnrtu])"
- + "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute"
- + "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])"
- + "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])"
- + "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])"
- + "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc"
- + "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live"
- + "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])"
- + "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba"
- + "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda"
- + "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar"
- + "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])"
- + "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk"
- + "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])"
- + "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka"
- + "|otsuka|ovh|om)"
- + "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography"
- + "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing"
- + "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property"
- + "|protection|pub|p[aefghklmnrstwy])"
- + "|(?:qpon|quebec|qa)"
- + "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals"
- + "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks"
- + "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])"
- + "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo"
- + "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security"
- + "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski"
- + "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting"
- + "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies"
- + "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])"
- + "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica"
- + "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools"
- + "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])"
- + "|(?:ubs|university|uno|uol|u[agksyz])"
- + "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin"
- + "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])"
- + "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill"
- + "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])"
- + "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434"
- + "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d"
- + "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431"
- + "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648"
- + "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629"
- + "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646"
- + "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633"
- + "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629"
- + "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646"
- + "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627"
- + "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924"
- + "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4"
- + "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd"
- + "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22"
- + "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c"
- + "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71"
- + "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063"
- + "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c"
- + "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c"
- + "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f"
- + "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc"
- + "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137"
- + "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox"
- + "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g"
- + "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim"
- + "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks"
- + "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a"
- + "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd"
- + "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h"
- + "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s"
- + "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c"
- + "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i"
- + "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d"
- + "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt"
- + "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e"
- + "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab"
- + "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema"
- + "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh"
- + "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c"
- + "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb"
- + "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a"
- + "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o"
- + "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)"
- + "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])"
- + "|(?:zara|zip|zone|zuerich|z[amw]))";
- /**
- * Kept for backward compatibility reasons.
- *
- * @deprecated Deprecated since it does not include all IRI characters defined in RFC 3987
- */
- @Deprecated
- public static final String GOOD_IRI_CHAR =
- "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
- public static final Pattern IP_ADDRESS
- = Pattern.compile(
- "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
- + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
- + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
- + "|[1-9][0-9]|[0-9]))");
-
- /**
- * IPv6 address matcher for
- * IPv6 addresses
- * zero compressed IPv6 addresses (section 2.2 of rfc5952)
- * link-local IPv6 addresses with zone index (section 11 of rfc4007)
- * IPv4-Embedded IPv6 Address (section 2 of rfc6052)
- * IPv4-mapped IPv6 addresses (section 2.1 of rfc2765)
- * IPv4-translated addresses (section 2.1 of rfc2765)
- *
- * Taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses/17871737#17871737
- */
- public static final Pattern IP6_ADDRESS
- = Pattern.compile(
- "\\[" +
- "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" +
- "([0-9a-fA-F]{1,4}:){1,7}:|" +
- "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" +
- "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" +
- "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" +
- "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" +
- "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" +
- "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" +
- ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" +
- "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" +
- "::(ffff(:0{1,4}){0,1}:){0,1}" +
- "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" +
- "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" +
- "([0-9a-fA-F]{1,4}:){1,4}:" +
- "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" +
- "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" +
- "\\]"
- );
- /**
- * Valid UCS characters defined in RFC 3987. Excludes space characters.
- */
- private static final String UCS_CHAR = "[" +
- "\u00A0-\uD7FF" +
- "\uF900-\uFDCF" +
- "\uFDF0-\uFFEF" +
- "\uD800\uDC00-\uD83F\uDFFD" +
- "\uD840\uDC00-\uD87F\uDFFD" +
- "\uD880\uDC00-\uD8BF\uDFFD" +
- "\uD8C0\uDC00-\uD8FF\uDFFD" +
- "\uD900\uDC00-\uD93F\uDFFD" +
- "\uD940\uDC00-\uD97F\uDFFD" +
- "\uD980\uDC00-\uD9BF\uDFFD" +
- "\uD9C0\uDC00-\uD9FF\uDFFD" +
- "\uDA00\uDC00-\uDA3F\uDFFD" +
- "\uDA40\uDC00-\uDA7F\uDFFD" +
- "\uDA80\uDC00-\uDABF\uDFFD" +
- "\uDAC0\uDC00-\uDAFF\uDFFD" +
- "\uDB00\uDC00-\uDB3F\uDFFD" +
- "\uDB44\uDC00-\uDB7F\uDFFD" +
- "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]";
- /**
- * Valid characters for IRI label defined in RFC 3987.
- */
- private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR;
- /**
- * Valid characters for IRI TLD defined in RFC 3987.
- */
- private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR;
- /**
- * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
- */
- private static final String IRI_LABEL =
- "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "\\-]{0,61}[" + LABEL_CHAR + "]){0,1}";
- /**
- * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
- */
- private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w";
- private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
- private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
- public static final Pattern DOMAIN_NAME
- = Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS +")");
- private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/";
- /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
- private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
- private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
- + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
- + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@";
- private static final String PORT_NUMBER = "\\:\\d{1,5}";
- private static final String PATH_CHAR = "(?:(?:[" + LABEL_CHAR
- + "\\;\\/\\?\\:\\@\\&\\=\\#\\~"
- + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_\\$])|(?:\\%[a-fA-F0-9]{2}))";
- private static final String PATH_AND_QUERY = "\\/" + PATH_CHAR + "*";
- /**
- * Regular expression pattern to match most part of RFC 3987
- * Internationalized URLs, aka IRIs.
- */
- public static final Pattern WEB_URL = Pattern.compile("("
- + "("
- + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?"
- + "(?:" + DOMAIN_NAME + ")"
- + "(?:" + PORT_NUMBER + ")?"
- + ")"
- + "(" + PATH_AND_QUERY + ")?"
- + WORD_BOUNDARY
- + ")");
- /**
- * Regular expression that matches known TLDs and punycode TLDs
- */
- private static final String STRICT_TLD = "(?:" +
- IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")";
- /**
- * Regular expression that matches host names using {@link #STRICT_TLD}
- */
- private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+"
- + STRICT_TLD + ")";
- /**
- * Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or
- * {@link #IP_ADDRESS}
- */
- private static final Pattern STRICT_DOMAIN_NAME
- = Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")");
- /**
- * Regular expression that matches domain names without a TLD
- */
- private static final String RELAXED_DOMAIN_NAME =
- "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")";
- /**
- * Regular expression to match strings that do not start with a supported protocol. The TLDs
- * are expected to be one of the known TLDs.
- */
- private static final String WEB_URL_WITHOUT_PROTOCOL = "("
- + WORD_BOUNDARY
- + "(? The pattern matches the following:
- *
- * - Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes
- * may follow.
- *
- Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes.
- *
- A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes.
- *
- */
- public static final Pattern PHONE
- = Pattern.compile( // sdd = space, dot, or dash
- "(\\+[0-9]+[\\- \\.]*)?" // +*
- + "(\\([0-9]+\\)[\\- \\.]*)?" // ()*
- + "([0-9][0-9\\- \\.]+[0-9])"); // +
-
- public static final Pattern TEL_URI =
- Pattern.compile("tel:(?:(?:\\+\\d+)|(?:\\d+;phone-context=" + PATH_CHAR + "+))");
-
- public static final Pattern SMS_URI =
- Pattern.compile("sms:(?:(?:\\+\\d+)|(?:\\d+;phone-context=" + PATH_CHAR + "+))");
-
- /**
- * Convenience method to take all of the non-null matching groups in a
- * regex Matcher and return them as a concatenated string.
- *
- * @param matcher The Matcher object from which grouped text will
- * be extracted
- *
- * @return A String comprising all of the non-null matched
- * groups concatenated together
- */
- public static final String concatGroups(Matcher matcher) {
- StringBuilder b = new StringBuilder();
- final int numGroups = matcher.groupCount();
- for (int i = 1; i <= numGroups; i++) {
- String s = matcher.group(i);
- if (s != null) {
- b.append(s);
- }
- }
- return b.toString();
- }
- /**
- * Convenience method to return only the digits and plus signs
- * in the matching string.
- *
- * @param matcher The Matcher object from which digits and plus will
- * be extracted
- *
- * @return A String comprising all of the digits and plus in
- * the match
- */
- public static final String digitsAndPlusOnly(Matcher matcher) {
- StringBuilder buffer = new StringBuilder();
- String matchingRegion = matcher.group();
- for (int i = 0, size = matchingRegion.length(); i < size; i++) {
- char character = matchingRegion.charAt(i);
- if (character == '+' || Character.isDigit(character)) {
- buffer.append(character);
- }
- }
- return buffer.toString();
- }
- /**
- * Do not create this static utility class.
- */
- private Patterns() {}
-}
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 8c9afc22e86909807f7f5629e428dcf22e61d9d2..56c1a1d426be37863ccf38f2a4b75c08ada71af9 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -8,7 +8,6 @@ import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Pair;
import android.widget.TextView;
-
import androidx.annotation.ColorInt;
import androidx.core.content.res.ResourcesCompat;
import androidx.annotation.ColorRes;
@@ -29,6 +28,9 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
+import de.gultsch.common.Linkify;
+import com.google.android.material.color.MaterialColors;
+import com.google.common.base.Strings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -43,39 +45,45 @@ import eu.siacs.conversations.entities.Reaction;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.worker.ExportBackupWorker;
import eu.siacs.conversations.xmpp.Jid;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
public class UIHelper {
- private static final List LOCATION_QUESTIONS = Arrays.asList(
- "where are you", //en
- "where are you now", //en
- "where are you right now", //en
- "whats your 20", //en
- "what is your 20", //en
- "what's your 20", //en
- "whats your twenty", //en
- "what is your twenty", //en
- "what's your twenty", //en
- "wo bist du", //de
- "wo bist du jetzt", //de
- "wo bist du gerade", //de
- "wo seid ihr", //de
- "wo seid ihr jetzt", //de
- "wo seid ihr gerade", //de
- "dónde estás", //es
- "donde estas" //es
- );
-
- private static final List PUNCTIONATION = Arrays.asList('.', ',', '?', '!', ';', ':');
-
- private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
- | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
- private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
- | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
+ private static final List LOCATION_QUESTIONS =
+ Arrays.asList(
+ "where are you", // en
+ "where are you now", // en
+ "where are you right now", // en
+ "whats your 20", // en
+ "what is your 20", // en
+ "what's your 20", // en
+ "whats your twenty", // en
+ "what is your twenty", // en
+ "what's your twenty", // en
+ "wo bist du", // de
+ "wo bist du jetzt", // de
+ "wo bist du gerade", // de
+ "wo seid ihr", // de
+ "wo seid ihr jetzt", // de
+ "wo seid ihr gerade", // de
+ "dónde estás", // es
+ "donde estas" // es
+ );
+
+ private static final List PUNCTIONATION =
+ Arrays.asList('.', ',', '?', '!', ';', ':');
+
+ private static final int SHORT_DATE_FLAGS =
+ DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
+ private static final int FULL_DATE_FLAGS =
+ DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
public static String readableTimeDifference(Context context, long time) {
return readableTimeDifference(context, time, false);
@@ -85,8 +93,7 @@ public class UIHelper {
return readableTimeDifference(context, time, true);
}
- private static String readableTimeDifference(Context context, long time,
- boolean fullDate) {
+ private static String readableTimeDifference(Context context, long time, boolean fullDate) {
if (time == 0) {
return context.getString(R.string.just_now);
}
@@ -103,11 +110,9 @@ public class UIHelper {
return df.format(date);
} else {
if (fullDate) {
- return DateUtils.formatDateTime(context, date.getTime(),
- FULL_DATE_FLAGS);
+ return DateUtils.formatDateTime(context, date.getTime(), FULL_DATE_FLAGS);
} else {
- return DateUtils.formatDateTime(context, date.getTime(),
- SHORT_DATE_FLAGS);
+ return DateUtils.formatDateTime(context, date.getTime(), SHORT_DATE_FLAGS);
}
}
}
@@ -126,8 +131,7 @@ public class UIHelper {
cal1.add(Calendar.DAY_OF_YEAR, -1);
cal2.setTime(new Date(date));
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
- && cal1.get(Calendar.DAY_OF_YEAR) == cal2
- .get(Calendar.DAY_OF_YEAR);
+ && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
}
public static boolean sameDay(long a, long b) {
@@ -140,8 +144,7 @@ public class UIHelper {
cal1.setTime(a);
cal2.setTime(b);
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
- && cal1.get(Calendar.DAY_OF_YEAR) == cal2
- .get(Calendar.DAY_OF_YEAR);
+ && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
}
public static String lastseen(Context context, boolean active, long time) {
@@ -157,13 +160,13 @@ public class UIHelper {
} else if (difference < 60 * 60 * 2) {
return context.getString(R.string.last_seen_hour);
} else if (difference < 60 * 60 * 24) {
- return context.getString(R.string.last_seen_hours,
- Math.round(difference / (60.0 * 60.0)));
+ return context.getString(
+ R.string.last_seen_hours, Math.round(difference / (60.0 * 60.0)));
} else if (difference < 60 * 60 * 48) {
return context.getString(R.string.last_seen_day);
} else {
- return context.getString(R.string.last_seen_days,
- Math.round(difference / (60.0 * 60.0 * 24.0)));
+ return context.getString(
+ R.string.last_seen_days, Math.round(difference / (60.0 * 60.0 * 24.0)));
}
}
@@ -177,7 +180,7 @@ public class UIHelper {
}
}
- public static int getColorForName(String name) {
+ public static int getColorForName(final String name) {
return XEP0392Helper.rgbFromNick(name);
}
@@ -185,7 +188,8 @@ public class UIHelper {
return getMessagePreview(context, message, 0);
}
- public static Pair getMessagePreview(final XmppConnectionService context, final Message message, @ColorInt int textColor) {
+ public static Pair getMessagePreview(
+ final XmppConnectionService context, final Message message, @ColorInt int textColor) {
final Transferable d = message.getTransferable();
final boolean moderated = message.getModerated() != null;
final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && message.getConversation().getMode() == Conversation.MODE_MULTI && context.isMucUserMuted(new MucOptions.User(null, message.getConversation().getJid(), message.getOccupantId(), null, null));
@@ -194,27 +198,43 @@ public class UIHelper {
} else if (d != null && !moderated) {
switch (d.getStatus()) {
case Transferable.STATUS_CHECKING:
- return new Pair<>(context.getString(R.string.checking_x,
- getFileDescriptionString(context, message)), true);
+ return new Pair<>(
+ context.getString(
+ R.string.checking_x,
+ getFileDescriptionString(context, message)),
+ true);
case Transferable.STATUS_DOWNLOADING:
- return new Pair<>(context.getString(R.string.receiving_x_file,
- getFileDescriptionString(context, message),
- d.getProgress()), true);
+ return new Pair<>(
+ context.getString(
+ R.string.receiving_x_file,
+ getFileDescriptionString(context, message),
+ d.getProgress()),
+ true);
case Transferable.STATUS_OFFER:
case Transferable.STATUS_OFFER_CHECK_FILESIZE:
- return new Pair<>(context.getString(R.string.x_file_offered_for_download,
- getFileDescriptionString(context, message)), true);
+ return new Pair<>(
+ context.getString(
+ R.string.x_file_offered_for_download,
+ getFileDescriptionString(context, message)),
+ true);
case Transferable.STATUS_FAILED:
return new Pair<>(context.getString(R.string.file_transmission_failed), true);
case Transferable.STATUS_CANCELLED:
- return new Pair<>(context.getString(R.string.file_transmission_cancelled), true);
+ return new Pair<>(
+ context.getString(R.string.file_transmission_cancelled), true);
case Transferable.STATUS_UPLOADING:
if (message.getStatus() == Message.STATUS_OFFERED) {
- return new Pair<>(context.getString(R.string.offering_x_file,
- getFileDescriptionString(context, message)), true);
+ return new Pair<>(
+ context.getString(
+ R.string.offering_x_file,
+ getFileDescriptionString(context, message)),
+ true);
} else {
- return new Pair<>(context.getString(R.string.sending_x_file,
- getFileDescriptionString(context, message)), true);
+ return new Pair<>(
+ context.getString(
+ R.string.sending_x_file,
+ getFileDescriptionString(context, message)),
+ true);
}
default:
return new Pair<>("", false);
@@ -235,18 +255,28 @@ public class UIHelper {
if (!rtpSessionStatus.successful && received) {
return new Pair<>(context.getString(R.string.missed_call), true);
} else {
- return new Pair<>(context.getString(received ? R.string.incoming_call : R.string.outgoing_call), true);
+ return new Pair<>(
+ context.getString(
+ received ? R.string.incoming_call : R.string.outgoing_call),
+ true);
}
} else {
final String body = MessageUtils.filterLtrRtl(message.getBody());
if (body.startsWith(Message.ME_COMMAND)) {
- return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND,
- UIHelper.getMessageDisplayName(message) + " "), false);
+ return new Pair<>(
+ body.replaceAll(
+ "^" + Message.ME_COMMAND,
+ UIHelper.getMessageDisplayName(message) + " "),
+ false);
} else if (message.isGeoUri()) {
return new Pair<>(context.getString(R.string.location), true);
- } else if (!moderated && (message.treatAsDownloadable() || MessageUtils.unInitiatedButKnownSize(message))) {
- return new Pair<>(context.getString(R.string.x_file_offered_for_download,
- getFileDescriptionString(context, message)), true);
+ } else if (!moderated && (message.treatAsDownloadable()
+ || MessageUtils.unInitiatedButKnownSize(message))) {
+ return new Pair<>(
+ context.getString(
+ R.string.x_file_offered_for_download,
+ getFileDescriptionString(context, message)),
+ true);
} else {
Drawable fallbackImg = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_photo_24dp, null);
fallbackImg.setBounds(0, 0, fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight());
@@ -255,7 +285,7 @@ public class UIHelper {
if (textColor != 0 && processMarkup) {
StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor, false);
}
- MyLinkify.addLinks(styledBody, message.getConversation().getAccount(), message.getConversation().getJid());
+ Linkify.addLinks(styledBody, message.getConversation().getAccount(), message.getConversation().getJid());
for (final android.text.style.QuoteSpan quote : Lists.reverse(Lists.newArrayList(styledBody.getSpans(0, styledBody.length(), android.text.style.QuoteSpan.class)))) {
int start = styledBody.getSpanStart(quote);
@@ -317,18 +347,18 @@ public class UIHelper {
return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input;
}
- public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos){
+ public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos) {
// true if not a single linebreak before current position
- for (int i = pos - 1; i >= 0; i--){
- if (body.charAt(i) != ' '){
+ for (int i = pos - 1; i >= 0; i--) {
+ if (body.charAt(i) != ' ') {
return false;
}
}
return true;
}
- public static boolean isPositionPrecededByLineStart(CharSequence body, int pos){
- if (isPositionPrecededByBodyStart(body, pos)){
+ public static boolean isPositionPrecededByLineStart(CharSequence body, int pos) {
+ if (isPositionPrecededByBodyStart(body, pos)) {
return true;
}
return body.charAt(pos - 1) == '\n';
@@ -376,7 +406,8 @@ public class UIHelper {
final char c = body.charAt(i);
if (Character.isWhitespace(c)) {
return false;
- } else if (QuoteHelper.isPositionQuoteCharacter(body, pos) || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) {
+ } else if (QuoteHelper.isPositionQuoteCharacter(body, pos)
+ || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) {
return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1));
}
}
@@ -439,6 +470,8 @@ public class UIHelper {
return context.getString(R.string.image);
} else if (mime.contains("pdf")) {
return context.getString(R.string.pdf_document);
+ } else if (MimeUtils.WORD_DOCUMENT_MIMES.contains(mime)) {
+ return context.getString(R.string.word_document);
} else if (mime.equals("application/vnd.android.package-archive")) {
return context.getString(R.string.apk);
} else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
@@ -447,7 +480,8 @@ public class UIHelper {
return context.getString(R.string.vcard);
} else if (mime.equals("text/x-vcalendar") || mime.equals("text/calendar")) {
return context.getString(R.string.event);
- } else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) {
+ } else if (mime.equals("application/epub+zip")
+ || mime.equals("application/vnd.amazon.mobi8-ebook")) {
return context.getString(R.string.ebook);
} else if (mime.equals("application/gpx+xml")) {
return context.getString(R.string.gpx_track);
@@ -488,7 +522,8 @@ public class UIHelper {
return contact != null ? contact.getDisplayName() : "";
}
} else {
- if (conversation instanceof Conversation && conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation instanceof Conversation
+ && conversation.getMode() == Conversation.MODE_MULTI) {
return ((Conversation) conversation).getMucOptions().getSelf().getNick();
} else {
final Account account = conversation.getAccount();
@@ -499,7 +534,6 @@ public class UIHelper {
} else {
return displayName;
}
-
}
}
}
@@ -520,7 +554,7 @@ public class UIHelper {
return conversation == null ? reaction.from.asBareJid().toString() : conversation.getAccount().getRoster().getContact(reaction.from).getDisplayName();
}
- public static String getMessageHint(final Context context,final Conversation conversation) {
+ public static String getMessageHint(final Context context, final Conversation conversation) {
return switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE -> {
if (Config.multipleEncryptionChoices()) {
@@ -557,10 +591,12 @@ public class UIHelper {
|| message.getType() != Message.TYPE_TEXT) {
return false;
}
- final String body = Strings.nullToEmpty(message.getBody())
- .trim()
- .toLowerCase(Locale.getDefault())
- .replace("?", "").replace("¿", "");
+ final String body =
+ Strings.nullToEmpty(message.getBody())
+ .trim()
+ .toLowerCase(Locale.getDefault())
+ .replace("?", "")
+ .replace("¿", "");
return LOCATION_QUESTIONS.contains(body);
}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
index 6a39a6aacb668b625ce8689ae50c63e21b1ad9e1..fec68360ee31ade6e1c113e6f9bf4cfdc3c50d95 100644
--- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java
+++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
@@ -45,19 +45,19 @@ public class XmppUri {
}
}
- public XmppUri(Uri uri) {
+ public XmppUri(final Uri uri) {
parse(uri);
}
- public XmppUri(Uri uri, boolean safeSource) {
+ public XmppUri(final Uri uri, final boolean safeSource) {
this.safeSource = safeSource;
parse(uri);
}
- private static Map parseParameters(final String query, final char seperator) {
+ private static Map parseParameters(final String query, final char separator) {
final ImmutableMap.Builder builder = new ImmutableMap.Builder<>();
final String[] pairs =
- query == null ? new String[0] : query.split(String.valueOf(seperator));
+ query == null ? new String[0] : query.split(String.valueOf(separator));
for (String pair : pairs) {
final String[] parts = pair.split("=", 2);
if (parts.length == 0) {
@@ -278,7 +278,8 @@ public class XmppUri {
public final String fingerprint;
final int deviceId;
- public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
+ public Fingerprint(
+ final FingerprintType type, final String fingerprint, final int deviceId) {
this.type = type;
this.fingerprint = fingerprint;
this.deviceId = deviceId;
diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java
index 7564fb451a7fe89849ba52eb0b02edf5783738c2..3330a7dc4a1e859e219df929ffd3a09057314bad 100644
--- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java
+++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java
@@ -16,6 +16,7 @@ import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
+import androidx.documentfile.provider.DocumentFile;
import androidx.work.ForegroundInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
@@ -26,6 +27,7 @@ import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gson.stream.JsonWriter;
+import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
@@ -34,12 +36,14 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
@@ -69,9 +73,9 @@ public class ExportBackupWorker extends Worker {
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.US);
- public static final String KEYTYPE = "AES";
- public static final String CIPHERMODE = "AES/GCM/NoPadding";
- public static final String PROVIDER = "BC";
+ private static final String KEY_TYPE = "AES";
+ private static final String CIPHER_MODE = "AES/GCM/NoPadding";
+ private static final String PROVIDER = "BC";
public static final String MIME_TYPE = "application/vnd.conversations.backup";
@@ -96,7 +100,7 @@ public class ExportBackupWorker extends Worker {
@Override
public Result doWork() {
setForegroundAsync(getForegroundInfo());
- final List files;
+ final List files;
try {
files = export();
} catch (final IOException
@@ -136,7 +140,7 @@ public class ExportBackupWorker extends Worker {
}
}
- private List export()
+ private List export()
throws IOException,
InvalidKeySpecException,
InvalidAlgorithmParameterException,
@@ -145,17 +149,19 @@ public class ExportBackupWorker extends Worker {
NoSuchAlgorithmException,
NoSuchProviderException {
final Context context = getApplicationContext();
+ final var appSettings = new AppSettings(context);
+ final var backupLocation = appSettings.getBackupLocation();
final var database = DatabaseBackend.getInstance(context);
final var accounts = database.getAccounts();
int count = 0;
final int max = accounts.size();
- final ImmutableList.Builder files = new ImmutableList.Builder<>();
+ final ImmutableList.Builder locations = new ImmutableList.Builder<>();
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
for (final Account account : accounts) {
if (isStopped()) {
Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have");
- return files.build();
+ return locations.build();
}
final String password = account.getPassword();
if (Strings.nullToEmpty(password).trim().isEmpty()) {
@@ -168,34 +174,24 @@ public class ExportBackupWorker extends Worker {
count++;
continue;
}
- final String filename =
- String.format(
- "%s.%s.ceb",
- account.getJid().asBareJid().toString(),
- DATE_FORMAT.format(new Date()));
- final File file = new File(FileBackend.getBackupDirectory(context), filename);
+ final Uri uri;
try {
- export(database, account, password, file, max, count);
+ uri = export(database, account, password, backupLocation, max, count);
} catch (final WorkStoppedException e) {
- if (file.delete()) {
- Log.d(
- Config.LOGTAG,
- "deleted in progress backup file " + file.getAbsolutePath());
- }
Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have");
- return files.build();
+ return locations.build();
}
- files.add(file);
+ locations.add(uri);
count++;
}
- return files.build();
+ return locations.build();
}
- private void export(
+ private Uri export(
final DatabaseBackend database,
final Account account,
final String password,
- final File file,
+ final Uri backupLocation,
final int max,
final int count)
throws IOException,
@@ -234,24 +230,48 @@ public class ExportBackupWorker extends Worker {
cancelPendingIntent)
.build());
final Progress progress = new Progress(notification, max, count);
- final File directory = file.getParentFile();
- if (directory != null && directory.mkdirs()) {
- Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath());
+ final String filename =
+ String.format(
+ "%s.%s.ceb",
+ account.getJid().asBareJid().toString(), DATE_FORMAT.format(new Date()));
+ final OutputStream outputStream;
+ final Uri location;
+ if ("file".equalsIgnoreCase(backupLocation.getScheme())) {
+ final File file = new File(backupLocation.getPath(), filename);
+ final File directory = file.getParentFile();
+ if (directory != null && directory.mkdirs()) {
+ Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath());
+ }
+ outputStream = new FileOutputStream(file);
+ location = Uri.fromFile(file);
+ } else {
+ final var tree = DocumentFile.fromTreeUri(context, backupLocation);
+ if (tree == null) {
+ throw new IOException(
+ String.format(
+ "DocumentFile.fromTreeUri returned null for %s", backupLocation));
+ }
+ final var file = tree.createFile(MIME_TYPE, filename);
+ if (file == null) {
+ throw new IOException(
+ String.format("Could not create %s in %s", filename, backupLocation));
+ }
+ location = file.getUri();
+ outputStream = context.getContentResolver().openOutputStream(location);
}
- final FileOutputStream fileOutputStream = new FileOutputStream(file);
- final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
+ final DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
final Cipher cipher =
Compatibility.twentyEight()
- ? Cipher.getInstance(CIPHERMODE)
- : Cipher.getInstance(CIPHERMODE, PROVIDER);
+ ? Cipher.getInstance(CIPHER_MODE)
+ : Cipher.getInstance(CIPHER_MODE, PROVIDER);
final byte[] key = getKey(password, salt);
- SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
+ SecretKeySpec keySpec = new SecretKeySpec(key, KEY_TYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
- CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
+ CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
final SQLiteDatabase db = database.getReadableDatabase();
@@ -267,13 +287,12 @@ public class ExportBackupWorker extends Worker {
SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
SQLiteAxolotlStore.SESSION_TABLENAME,
SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
- throwIfWorkStopped();
+ throwIfWorkStopped(location);
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
}
writer.flush();
writer.close();
- mediaScannerScanFile(file);
- Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
+ return location;
}
private NotificationCompat.Builder getNotification() {
@@ -289,8 +308,20 @@ public class ExportBackupWorker extends Worker {
return notification;
}
- private void throwIfWorkStopped() throws WorkStoppedException {
+ private void throwIfWorkStopped(final Uri location) throws WorkStoppedException {
if (isStopped()) {
+ if ("file".equalsIgnoreCase(location.getScheme())) {
+ final var file = new File(location.getPath());
+ if (file.delete()) {
+ Log.d(Config.LOGTAG, "deleted " + file.getAbsolutePath());
+ }
+ } else {
+ final var documentFile =
+ DocumentFile.fromSingleUri(getApplicationContext(), location);
+ if (documentFile != null && documentFile.delete()) {
+ Log.d(Config.LOGTAG, "deleted " + location);
+ }
+ }
throw new WorkStoppedException();
}
}
@@ -495,16 +526,19 @@ public class ExportBackupWorker extends Worker {
.getEncoded();
}
- private void notifySuccess(final List files) {
+ private void notifySuccess(final List locations) {
final var context = getApplicationContext();
- final String path = FileBackend.getBackupDirectory(context).getAbsolutePath();
-
- final var openFolderIntent = getOpenFolderIntent(path);
-
+ final var appSettings = new AppSettings(context);
+ final String path = appSettings.getBackupLocationAsPath();
final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
final ArrayList uris = new ArrayList<>();
- for (final File file : files) {
- uris.add(FileBackend.getUriForFile(context, file, file.getName()));
+ for (final Uri uri : locations) {
+ if ("file".equalsIgnoreCase(uri.getScheme())) {
+ final var file = new File(uri.getPath());
+ uris.add(FileBackend.getUriForFile(context, file, file.getName()));
+ } else {
+ uris.add(uri);
+ }
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -514,8 +548,8 @@ public class ExportBackupWorker extends Worker {
final var shareFilesIntent =
PendingIntent.getActivity(context, 190, chooser, PENDING_INTENT_FLAGS);
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup");
- mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title))
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "backup");
+ builder.setContentTitle(context.getString(R.string.notification_backup_created_title))
.setContentText(
context.getString(R.string.notification_backup_created_subtitle, path))
.setStyle(
@@ -523,60 +557,17 @@ public class ExportBackupWorker extends Worker {
.bigText(
context.getString(
R.string.notification_backup_created_subtitle,
- FileBackend.getBackupDirectory(context)
- .getAbsolutePath())))
+ path)))
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_archive_24dp);
- if (openFolderIntent.isPresent()) {
- mBuilder.setContentIntent(openFolderIntent.get());
- } else {
- Log.w(Config.LOGTAG, "no app can display folders");
- }
-
- mBuilder.addAction(
+ builder.addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.share_backup_files),
shareFilesIntent);
+ builder.setLocalOnly(true);
final var notificationManager = context.getSystemService(NotificationManager.class);
- notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, mBuilder.build());
- }
-
- private Optional getOpenFolderIntent(final String path) {
- final var context = getApplicationContext();
- for (final Intent intent : getPossibleFileOpenIntents(context, path)) {
- if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) {
- return Optional.of(
- PendingIntent.getActivity(context, 189, intent, PENDING_INTENT_FLAGS));
- }
- }
- return Optional.absent();
- }
-
- private static List getPossibleFileOpenIntents(
- final Context context, final String path) {
-
- // http://www.openintents.org/action/android-intent-action-view/file-directory
- // do not use 'vnd.android.document/directory' since this will trigger system file manager
- final Intent openIntent = new Intent(Intent.ACTION_VIEW);
- openIntent.addCategory(Intent.CATEGORY_DEFAULT);
- if (Compatibility.runsAndTargetsTwentyFour(context)) {
- openIntent.setType("resource/folder");
- } else {
- openIntent.setDataAndType(Uri.parse("file://" + path), "resource/folder");
- }
- openIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", path);
-
- final Intent amazeIntent = new Intent(Intent.ACTION_VIEW);
- amazeIntent.setDataAndType(Uri.parse("com.amaze.filemanager:" + path), "resource/folder");
-
- // will open a file manager at root and user can navigate themselves
- final Intent systemFallBack = new Intent(Intent.ACTION_VIEW);
- systemFallBack.addCategory(Intent.CATEGORY_DEFAULT);
- systemFallBack.setData(
- Uri.parse("content://com.android.externalstorage.documents/root/primary"));
-
- return Arrays.asList(openIntent, amazeIntent, systemFallBack);
+ notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, builder.build());
}
private static class Progress {
diff --git a/src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java
new file mode 100644
index 0000000000000000000000000000000000000000..556748a72362b2b04abe6c98f4bde5ad9035b6c9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java
@@ -0,0 +1,389 @@
+package eu.siacs.conversations.worker;
+
+import static eu.siacs.conversations.utils.Compatibility.s;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.work.Data;
+import androidx.work.ForegroundInfo;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.CountingInputStream;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.AccountUtils;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.xmpp.Jid;
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.ZipException;
+import javax.crypto.BadPaddingException;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherInputStream;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class ImportBackupWorker extends Worker {
+
+ public static final String TAG_IMPORT_BACKUP = "tag-import-backup";
+
+ private static final String DATA_KEY_PASSWORD = "password";
+ private static final String DATA_KEY_URI = "uri";
+ private static final String DATA_KEY_INCLUDE_OMEMO = "omemo";
+
+ private static final Collection OMEMO_TABLE_LIST =
+ Arrays.asList(
+ SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.IDENTITIES_TABLENAME);
+
+ private static final List TABLE_ALLOW_LIST =
+ new ImmutableList.Builder()
+ .add(Account.TABLENAME, Conversation.TABLENAME, Message.TABLENAME)
+ .addAll(OMEMO_TABLE_LIST)
+ .build();
+
+ private static final Pattern COLUMN_PATTERN = Pattern.compile("^[a-zA-Z_]+$");
+
+ private static final int NOTIFICATION_ID = 21;
+
+ private final String password;
+ private final Uri uri;
+ private final boolean includeOmemo;
+
+ public ImportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ final var inputData = workerParams.getInputData();
+ this.password = inputData.getString(DATA_KEY_PASSWORD);
+ this.uri = Uri.parse(inputData.getString(DATA_KEY_URI));
+ this.includeOmemo = inputData.getBoolean(DATA_KEY_INCLUDE_OMEMO, true);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ setForegroundAsync(
+ new ForegroundInfo(NOTIFICATION_ID, createImportBackupNotification(1, 0)));
+ final Result result;
+ try {
+ result = importBackup(this.uri, this.password);
+ } catch (final FileNotFoundException e) {
+ return failure(Reason.FILE_NOT_FOUND);
+ } catch (final Exception e) {
+ Log.d(Config.LOGTAG, "error restoring backup " + uri, e);
+ final Throwable throwable = e.getCause();
+ if (throwable instanceof BadPaddingException || e instanceof ZipException) {
+ return failure(Reason.DECRYPTION_FAILED);
+ } else {
+ return failure(Reason.GENERIC);
+ }
+ } finally {
+ getApplicationContext()
+ .getSystemService(NotificationManager.class)
+ .cancel(NOTIFICATION_ID);
+ }
+
+ return result;
+ }
+
+ private Result importBackup(final Uri uri, final String password)
+ throws IOException, InvalidKeySpecException {
+ final var context = getApplicationContext();
+ final var database = DatabaseBackend.getInstance(context);
+ Log.d(Config.LOGTAG, "importing backup from " + uri);
+ final Stopwatch stopwatch = Stopwatch.createStarted();
+ final SQLiteDatabase db = database.getWritableDatabase();
+ final InputStream inputStream;
+ final String path = uri.getPath();
+ final long fileSize;
+ if ("file".equals(uri.getScheme()) && path != null) {
+ final File file = new File(path);
+ inputStream = new FileInputStream(file);
+ fileSize = file.length();
+ } else {
+ final Cursor returnCursor =
+ context.getContentResolver().query(uri, null, null, null, null);
+ if (returnCursor == null) {
+ fileSize = 0;
+ } else {
+ returnCursor.moveToFirst();
+ fileSize =
+ returnCursor.getLong(
+ returnCursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
+ returnCursor.close();
+ }
+ inputStream = context.getContentResolver().openInputStream(uri);
+ }
+ if (inputStream == null) {
+ return failure(Reason.FILE_NOT_FOUND);
+ }
+ final CountingInputStream countingInputStream = new CountingInputStream(inputStream);
+ final DataInputStream dataInputStream = new DataInputStream(countingInputStream);
+ final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ Log.d(Config.LOGTAG, backupFileHeader.toString());
+
+ final var accounts = database.getAccountJids(false);
+
+ if (QuickConversationsService.isQuicksy() && !accounts.isEmpty()) {
+ return failure(Reason.ACCOUNT_ALREADY_EXISTS);
+ }
+
+ if (accounts.contains(backupFileHeader.getJid())) {
+ return failure(Reason.ACCOUNT_ALREADY_EXISTS);
+ }
+
+ final byte[] key = ExportBackupWorker.getKey(password, backupFileHeader.getSalt());
+
+ final AEADBlockCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
+ cipher.init(
+ false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));
+ final CipherInputStream cipherInputStream =
+ new CipherInputStream(countingInputStream, cipher);
+
+ final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
+ final BufferedReader reader =
+ new BufferedReader(new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8));
+ final JsonReader jsonReader = new JsonReader(reader);
+ if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
+ jsonReader.beginArray();
+ } else {
+ throw new IllegalStateException("Backup file did not begin with array");
+ }
+ db.beginTransaction();
+ while (jsonReader.hasNext()) {
+ if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
+ importRow(db, jsonReader, backupFileHeader.getJid(), password);
+ } else if (jsonReader.peek() == JsonToken.END_ARRAY) {
+ jsonReader.endArray();
+ continue;
+ }
+ updateImportBackupNotification(fileSize, countingInputStream.getCount());
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ final Jid jid = backupFileHeader.getJid();
+ final Cursor countCursor =
+ db.rawQuery(
+ "select count(messages.uuid) from messages join conversations on"
+ + " conversations.uuid=messages.conversationUuid join accounts on"
+ + " conversations.accountUuid=accounts.uuid where"
+ + " accounts.username=? and accounts.server=?",
+ new String[] {jid.getLocal(), jid.getDomain().toString()});
+ countCursor.moveToFirst();
+ final int count = countCursor.getInt(0);
+ Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop()));
+ countCursor.close();
+ stopBackgroundService();
+ notifySuccess();
+ return Result.success();
+ }
+
+ private void importRow(
+ final SQLiteDatabase db,
+ final JsonReader jsonReader,
+ final Jid account,
+ final String passphrase)
+ throws IOException {
+ jsonReader.beginObject();
+ final String firstParameter = jsonReader.nextName();
+ if (!firstParameter.equals("table")) {
+ throw new IllegalStateException("Expected key 'table'");
+ }
+ final String table = jsonReader.nextString();
+ if (!TABLE_ALLOW_LIST.contains(table)) {
+ throw new IOException(String.format("%s is not recognized for import", table));
+ }
+ final ContentValues contentValues = new ContentValues();
+ final String secondParameter = jsonReader.nextName();
+ if (!secondParameter.equals("values")) {
+ throw new IllegalStateException("Expected key 'values'");
+ }
+ jsonReader.beginObject();
+ while (jsonReader.peek() != JsonToken.END_OBJECT) {
+ final String name = jsonReader.nextName();
+ if (COLUMN_PATTERN.matcher(name).matches()) {
+ if (jsonReader.peek() == JsonToken.NULL) {
+ jsonReader.nextNull();
+ contentValues.putNull(name);
+ } else if (jsonReader.peek() == JsonToken.NUMBER) {
+ contentValues.put(name, jsonReader.nextLong());
+ } else {
+ contentValues.put(name, jsonReader.nextString());
+ }
+ } else {
+ throw new IOException(String.format("Unexpected column name %s", name));
+ }
+ }
+ jsonReader.endObject();
+ jsonReader.endObject();
+ if (Account.TABLENAME.equals(table)) {
+ final Jid jid =
+ Jid.of(
+ contentValues.getAsString(Account.USERNAME),
+ contentValues.getAsString(Account.SERVER),
+ null);
+ final String password = contentValues.getAsString(Account.PASSWORD);
+ if (QuickConversationsService.isQuicksy()) {
+ if (!jid.getDomain().equals(Config.QUICKSY_DOMAIN)) {
+ throw new IOException("Trying to restore non Quicksy account on Quicksy");
+ }
+ }
+ if (jid.equals(account) && passphrase.equals(password)) {
+ Log.d(Config.LOGTAG, "jid and password from backup header had matching row");
+ } else {
+ throw new IOException("jid or password in table did not match backup");
+ }
+ final var keys = Account.parseKeys(contentValues.getAsString(Account.KEYS));
+ final var deviceId = keys.optString(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID);
+ final var importReadyKeys = new JSONObject();
+ if (!Strings.isNullOrEmpty(deviceId) && this.includeOmemo) {
+ try {
+ importReadyKeys.put(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID, deviceId);
+ } catch (final JSONException e) {
+ Log.e(Config.LOGTAG, "error writing omemo registration id", e);
+ }
+ }
+ contentValues.put(Account.KEYS, importReadyKeys.toString());
+ }
+ if (this.includeOmemo) {
+ db.insert(table, null, contentValues);
+ } else {
+ if (OMEMO_TABLE_LIST.contains(table)) {
+ if (SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(table)
+ && contentValues.getAsInteger(SQLiteAxolotlStore.OWN) == 0) {
+ db.insert(table, null, contentValues);
+ } else {
+ Log.d(Config.LOGTAG, "skipping over omemo key material in table " + table);
+ }
+ } else {
+ db.insert(table, null, contentValues);
+ }
+ }
+ }
+
+ private void stopBackgroundService() {
+ final var intent = new Intent(getApplicationContext(), XmppConnectionService.class);
+ getApplicationContext().stopService(intent);
+ }
+
+ private void updateImportBackupNotification(final long total, final long current) {
+ final int max;
+ final int progress;
+ if (total == 0) {
+ max = 1;
+ progress = 0;
+ } else {
+ max = 100;
+ progress = (int) (current * 100 / total);
+ }
+ getApplicationContext()
+ .getSystemService(NotificationManager.class)
+ .notify(NOTIFICATION_ID, createImportBackupNotification(max, progress));
+ }
+
+ private Notification createImportBackupNotification(final int max, final int progress) {
+ final var context = getApplicationContext();
+ final var builder = new NotificationCompat.Builder(getApplicationContext(), "backup");
+ builder.setContentTitle(context.getString(R.string.restoring_backup))
+ .setSmallIcon(R.drawable.ic_unarchive_24dp)
+ .setProgress(max, progress, max == 1 && progress == 0);
+ return builder.build();
+ }
+
+ private void notifySuccess() {
+ final var context = getApplicationContext();
+ final var builder = new NotificationCompat.Builder(context, "backup");
+ builder.setContentTitle(context.getString(R.string.notification_restored_backup_title))
+ .setContentText(context.getString(R.string.notification_restored_backup_subtitle))
+ .setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_unarchive_24dp);
+ if (QuickConversationsService.isConversations()
+ && AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) {
+ builder.setContentText(
+ context.getString(R.string.notification_restored_backup_subtitle));
+ builder.setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 145,
+ new Intent(context, AccountUtils.MANAGE_ACCOUNT_ACTIVITY),
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT));
+ }
+ getApplicationContext()
+ .getSystemService(NotificationManager.class)
+ .notify(NOTIFICATION_ID + 2, builder.build());
+ }
+
+ public static Data data(final String password, final Uri uri, final boolean includeOmemo) {
+ return new Data.Builder()
+ .putString(DATA_KEY_PASSWORD, password)
+ .putString(DATA_KEY_URI, uri.toString())
+ .putBoolean(DATA_KEY_INCLUDE_OMEMO, includeOmemo)
+ .build();
+ }
+
+ private static Result failure(final Reason reason) {
+ return Result.failure(new Data.Builder().putString("reason", reason.toString()).build());
+ }
+
+ public enum Reason {
+ ACCOUNT_ALREADY_EXISTS,
+ DECRYPTION_FAILED,
+ FILE_NOT_FOUND,
+ GENERIC;
+
+ public static Reason valueOfOrGeneric(final String value) {
+ if (Strings.isNullOrEmpty(value)) {
+ return GENERIC;
+ }
+ try {
+ return valueOf(value);
+ } catch (final IllegalArgumentException e) {
+ return GENERIC;
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 3b42121bc8c47c5fa6fb7335562cc5b4b983bae0..e75ba27c78bb518638dcbe14c26f1aaa881d5715 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -70,6 +70,7 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
+import de.gultsch.common.Patterns;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
@@ -98,7 +99,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SSLSockets;
@@ -146,7 +146,6 @@ import im.conversations.android.xmpp.model.sm.StreamManagement;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Presence;
import im.conversations.android.xmpp.model.stanza.Stanza;
-import im.conversations.android.xmpp.model.streams.Features;
import im.conversations.android.xmpp.model.streams.StreamError;
import im.conversations.android.xmpp.model.tls.Proceed;
import im.conversations.android.xmpp.model.tls.StartTls;
@@ -1056,7 +1055,7 @@ public class XmppConnection implements Runnable {
if (Strings.isNullOrEmpty(text)) {
throw new StateChangingException(Account.State.UNAUTHORIZED);
}
- final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
+ final Matcher matcher = Patterns.URI_HTTP.matcher(text);
if (matcher.find()) {
final HttpUrl url;
try {
@@ -1961,7 +1960,7 @@ public class XmppConnection implements Runnable {
if (url != null) {
setAccountCreationFailed(url);
} else if (instructions != null) {
- final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
+ final Matcher matcher = Patterns.URI_HTTP.matcher(instructions);
if (matcher.find()) {
setAccountCreationFailed(
instructions.substring(matcher.start(), matcher.end()));
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
index 5149470f09bca5fe0ef6eb9c494a407fac5a30cc..cc383be716bab5e16a06fbfd98a13b2c3a72c77e 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
@@ -1,98 +1,21 @@
package eu.siacs.conversations.xmpp.jingle;
-import android.util.Log;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.primitives.Ints;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.utils.IP;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.disco.external.Services;
import im.conversations.android.xmpp.model.stanza.Iq;
-
-import org.webrtc.PeerConnection;
-
-import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
-import java.util.List;
+import org.webrtc.PeerConnection;
public final class IceServers {
- public static List parse(final Iq response) {
- ImmutableList.Builder listBuilder = new ImmutableList.Builder<>();
- if (response.getType() == Iq.Type.RESULT) {
- final Element services =
- response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
- final List children =
- services == null ? Collections.emptyList() : services.getChildren();
- for (final Element child : children) {
- if ("service".equals(child.getName())) {
- final String type = child.getAttribute("type");
- final String host = child.getAttribute("host");
- final String sport = child.getAttribute("port");
- final Integer port = sport == null ? null : Ints.tryParse(sport);
- final String transport = child.getAttribute("transport");
- final String username = child.getAttribute("username");
- final String password = child.getAttribute("password");
- if (Strings.isNullOrEmpty(host) || port == null) {
- continue;
- }
- if (port < 0 || port > 65535) {
- continue;
- }
-
- if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type)
- && Arrays.asList("udp", "tcp").contains(transport)) {
- if (Arrays.asList("stuns", "turns").contains(type)
- && "udp".equals(transport)) {
- Log.w(
- Config.LOGTAG,
- "skipping invalid combination of udp/tls in external services");
- continue;
- }
-
- // STUN URLs do not support a query section since M110
- final String uri;
- if (Arrays.asList("stun", "stuns").contains(type)) {
- uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
- } else {
- uri =
- String.format(
- "%s:%s:%s?transport=%s",
- type, IP.wrapIPv6(host), port, transport);
- }
-
- final PeerConnection.IceServer.Builder iceServerBuilder =
- PeerConnection.IceServer.builder(uri);
- iceServerBuilder.setTlsCertPolicy(
- PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK);
- if (username != null && password != null) {
- iceServerBuilder.setUsername(username);
- iceServerBuilder.setPassword(password);
- } else if (Arrays.asList("turn", "turns").contains(type)) {
- // The WebRTC spec requires throwing an
- // InvalidAccessError when username (from libwebrtc
- // source coder)
- // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
- Log.w(
- Config.LOGTAG,
- "skipping "
- + type
- + "/"
- + transport
- + " without username and password");
- continue;
- }
- final PeerConnection.IceServer iceServer =
- iceServerBuilder.createIceServer();
- Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
- listBuilder.add(iceServer);
- }
- }
- }
+ public static Collection parse(final Iq response) {
+ if (response.getType() != Iq.Type.RESULT) {
+ return Collections.emptySet();
+ }
+ final var services = response.getExtension(Services.class);
+ if (services == null) {
+ return Collections.emptySet();
}
- return listBuilder.build();
+ return services.getIceServers();
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
index ca035d5dfc7a7ab00c165c14e6b6db78d95a9f35..fbaf7ffe3e7a52d5a9148cc780cb0d6135448912 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
@@ -355,6 +355,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
Log.d(
Config.LOGTAG,
"got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage));
+ // TODO store hashes if there are any
setFileOffer(file);
if (keyTransportMessage != null) {
this.transportSecurity =
@@ -548,10 +549,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
private void receiveSessionInfoChecksum(final FileTransferDescription.Checksum checksum) {
Log.d(Config.LOGTAG, "received checksum " + checksum);
+ // TODO check that we are receiver
+ // TODO store hashes
}
private void receiveSessionInfoReceived(final FileTransferDescription.Received received) {
Log.d(Config.LOGTAG, "peer confirmed received " + received);
+ // TODO check that we are sender
}
private synchronized void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) {
@@ -902,6 +906,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
sendSessionInfoChecksum(hashes);
} else {
Log.d(Config.LOGTAG, "file transfer complete " + hashes);
+ // TODO compare with stored file hashes
sendFileSessionInfoReceived();
terminateTransport();
messageReceivedSuccess();
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
index 806c5d9b58b0e6e7c557f7901799939c3fa329ca..f3d892ffd887a0fe9d00f6d3f2c3917d82188075 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
@@ -47,6 +47,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed;
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import im.conversations.android.xmpp.model.disco.external.Services;
import im.conversations.android.xmpp.model.jingle.Jingle;
import im.conversations.android.xmpp.model.stanza.Iq;
@@ -1356,7 +1357,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private synchronized void sendSessionAccept(
final Set media,
final SessionDescription offer,
- final List iceServers) {
+ final Collection iceServers) {
if (isTerminated()) {
Log.w(
Config.LOGTAG,
@@ -1840,7 +1841,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private synchronized void sendSessionInitiate(
final Set media,
final State targetState,
- final List iceServers) {
+ final Collection iceServers) {
if (isTerminated()) {
Log.w(
Config.LOGTAG,
@@ -2337,7 +2338,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void setupWebRTC(
final Set media,
- final List iceServers,
+ final Collection iceServers,
final boolean trickle)
throws WebRTCWrapper.InitializationException {
this.jingleConnectionManager.ensureConnectionIsRegistered(this);
@@ -2841,7 +2842,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) {
final Iq request = new Iq(Iq.Type.GET);
request.setTo(id.account.getDomain());
- request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+ request.addExtension(new Services());
xmppConnectionService.sendIqPacket(
id.account,
request,
@@ -2860,7 +2861,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
Log.w(
Config.LOGTAG,
id.account.getJid().asBareJid() + ": has no external service discovery");
- onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
+ onIceServersDiscovered.onIceServersDiscovered(Collections.emptySet());
}
}
@@ -2978,6 +2979,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private interface OnIceServersDiscovered {
- void onIceServersDiscovered(List iceServers);
+ void onIceServersDiscovered(Collection iceServers);
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
index 24e82bc4f6c24003b20413ac8a70bf3a4d0db73b..f573dc30e666aea33d31590a37760efe43b11dca 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
@@ -12,7 +12,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
-
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
@@ -21,14 +20,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import im.conversations.android.xmpp.model.jingle.Jingle;
-
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-
import javax.annotation.Nonnull;
public class RtpContentMap extends AbstractContentMap {
@@ -100,7 +97,7 @@ public class RtpContentMap extends AbstractContentMap> entry :
@@ -119,7 +116,8 @@ public class RtpContentMap extends AbstractContentMap descriptionTransport =
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
index 025a7acc917fd0defb95953556b9b15f99cb271a..b981472d535cce394102ff39708ae9d577e784dc 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
@@ -2,9 +2,7 @@ package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import android.util.Pair;
-
import androidx.annotation.NonNull;
-
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
@@ -12,7 +10,6 @@ import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
@@ -21,7 +18,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
-
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -206,6 +202,10 @@ public class SessionDescription {
entry : contentMap.contents.entrySet()) {
final String name = entry.getKey();
checkNoWhitespace(name, "content name must not contain any whitespace");
+ // https://groups.google.com/g/discuss-webrtc/c/VG406JMTBI4/m/MrSex_q7AgAJ
+ if (name.length() > 16) {
+ throw new IllegalArgumentException("mid should not be longer than 16 chars");
+ }
final DescriptionTransport descriptionTransport =
entry.getValue();
final RtpDescription description = descriptionTransport.description;
@@ -226,7 +226,7 @@ public class SessionDescription {
if (parameters.size() == 1) {
mediaAttributes.put(
"fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
- } else if (parameters.size() > 0) {
+ } else if (!parameters.isEmpty()) {
mediaAttributes.put(
"fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
}
@@ -306,7 +306,7 @@ public class SessionDescription {
"A SSRC group is missing semantics attribute");
}
checkNoWhitespace(semantics, "source group semantics must not contain whitespace");
- if (groups.size() == 0) {
+ if (groups.isEmpty()) {
throw new IllegalArgumentException("A SSRC group is missing SSRC ids");
}
for (final String source : groups) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
index 79ea3580c4b3fdb0a3abddff5fae8ca034646d56..cbcd596dd44e145a1d4ed8af224f956a7a0b31d1 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
@@ -5,19 +5,28 @@ import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Build;
import android.util.Log;
-
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.XmppConnectionService;
-
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.CandidatePairChangeEvent;
@@ -261,13 +270,13 @@ public class WebRTCWrapper {
synchronized void initializePeerConnection(
final Set media,
- final List iceServers,
+ final Collection iceServers,
final boolean trickle)
throws InitializationException {
Preconditions.checkState(this.eglBase != null);
Preconditions.checkNotNull(media);
Preconditions.checkArgument(
- media.size() > 0, "media can not be empty when initializing peer connection");
+ !media.isEmpty(), "media can not be empty when initializing peer connection");
final boolean setUseHardwareAcousticEchoCanceler =
!HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
Log.d(
@@ -397,16 +406,16 @@ public class WebRTCWrapper {
if (videoSourceWrapper != null) {
try {
videoSourceWrapper.stopCapture();
- } catch (InterruptedException e) {
- e.printStackTrace();
+ } catch (final InterruptedException e) {
+ Log.e(Config.LOGTAG, "could not stop capturing video source", e);
}
}
}
public static PeerConnection.RTCConfiguration buildConfiguration(
- final List iceServers, final boolean trickle) {
+ final Collection iceServers, final boolean trickle) {
final PeerConnection.RTCConfiguration rtcConfig =
- new PeerConnection.RTCConfiguration(iceServers);
+ new PeerConnection.RTCConfiguration(ImmutableList.copyOf(iceServers));
rtcConfig.tcpCandidatePolicy =
PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp
if (trickle) {
@@ -423,7 +432,7 @@ public class WebRTCWrapper {
}
void reconfigurePeerConnection(
- final List iceServers, final boolean trickle) {
+ final Set iceServers, final boolean trickle) {
requirePeerConnection().setConfiguration(buildConfiguration(iceServers, trickle));
}
@@ -809,7 +818,7 @@ public class WebRTCWrapper {
}
}
- static class InitializationException extends Exception {
+ public static class InitializationException extends Exception {
private InitializationException(final String message, final Throwable throwable) {
super(message, throwable);
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
index c2afe72f625614e09d727952ee1b7d0cc625b5dc..50d3e81c95a098eb816852d32e46cd633362d0b0 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
@@ -1,28 +1,23 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Log;
-
import androidx.annotation.NonNull;
-
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
-import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
-
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -104,7 +99,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
for (final Element child : getChildren()) {
if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace())
&& IceOption.WELL_KNOWN.contains(child.getName())) {
- optionBuilder.add(child.getName());
+ optionBuilder.add(
+ SessionDescription.checkNoWhitespace(
+ child.getName(), "Ice options should not contain whitespace"));
}
}
return optionBuilder.build();
@@ -162,7 +159,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
transportInfo.setAttributes(new Hashtable<>(getAttributes()));
transportInfo.setChildren(this.getChildren());
- for(final Candidate candidate : candidates) {
+ for (final Candidate candidate : candidates) {
transportInfo.addChild(candidate);
}
return transportInfo;
@@ -223,7 +220,8 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return null;
}
- public static Candidate fromSdpAttributeValue(final String value, final String currentUfrag) {
+ public static Candidate fromSdpAttributeValue(
+ final String value, final String currentUfrag) {
final String[] segments = value.split(" ");
if (segments.length < 6) {
return null;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java
index 5fcc6697f7cbb5f9f608185e3fc47fe3fcd14e14..d214c8b74ec879135335c9f1005c059b31361a5f 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java
@@ -1,25 +1,22 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Pair;
-
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
-
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.Media;
+import eu.siacs.conversations.xmpp.jingle.SessionDescription;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.jingle.Media;
-import eu.siacs.conversations.xmpp.jingle.SessionDescription;
-
public class RtpDescription extends GenericDescription {
private RtpDescription(final String media) {
@@ -287,7 +284,7 @@ public class RtpDescription extends GenericDescription {
final String channels = this.getAttribute("channels");
if (channels == null) {
return 1; // The number of channels; if omitted, it MUST be assumed to contain one
- // channel
+ // channel
}
try {
return Integer.parseInt(channels);
@@ -532,13 +529,17 @@ public class RtpDescription extends GenericDescription {
}
public List getSsrcs() {
- ImmutableList.Builder builder = new ImmutableList.Builder<>();
+ final ImmutableList.Builder builder = new ImmutableList.Builder<>();
for (Element child : getChildren()) {
if ("source".equals(child.getName())) {
final String ssrc = child.getAttribute("ssrc");
- if (ssrc != null) {
- builder.add(ssrc);
+ if (Strings.isNullOrEmpty(ssrc)) {
+ continue;
}
+ builder.add(
+ SessionDescription.checkNoNewline(
+ ssrc,
+ "Source Specific media attributes can not contain newline"));
}
}
return builder.build();
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java
index 95b15438460b50ac229ed522162e714f820a48d6..80b0322f4727cf418d7f37de58d9cb6feb1ddbf6 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java
@@ -5,7 +5,6 @@ import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription;
import android.content.Context;
import android.util.Log;
-
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Closeables;
@@ -13,26 +12,15 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.IceServers;
import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
-
+import im.conversations.android.xmpp.model.disco.external.Services;
import im.conversations.android.xmpp.model.stanza.Iq;
-
-import org.webrtc.CandidatePairChangeEvent;
-import org.webrtc.DataChannel;
-import org.webrtc.IceCandidate;
-import org.webrtc.MediaStream;
-import org.webrtc.PeerConnection;
-import org.webrtc.PeerConnectionFactory;
-import org.webrtc.SessionDescription;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
@@ -42,6 +30,7 @@ import java.io.PipedOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
+import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -52,8 +41,14 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
-
import javax.annotation.Nonnull;
+import org.webrtc.CandidatePairChangeEvent;
+import org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SessionDescription;
public class WebRTCDataChannelTransport implements Transport {
@@ -229,16 +224,16 @@ public class WebRTCDataChannelTransport implements Transport {
}
}
- private ListenableFuture> getIceServers() {
+ private ListenableFuture> getIceServers() {
if (Config.DISABLE_PROXY_LOOKUP) {
- return Futures.immediateFuture(Collections.emptyList());
+ return Futures.immediateFuture(Collections.emptySet());
}
if (xmppConnection.getFeatures().externalServiceDiscovery()) {
- final SettableFuture> iceServerFuture =
+ final SettableFuture> iceServerFuture =
SettableFuture.create();
final Iq request = new Iq(Iq.Type.GET);
request.setTo(this.account.getDomain());
- request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+ request.addExtension(new Services());
xmppConnection.sendIqPacket(
request,
(response) -> {
@@ -254,12 +249,12 @@ public class WebRTCDataChannelTransport implements Transport {
});
return iceServerFuture;
} else {
- return Futures.immediateFuture(Collections.emptyList());
+ return Futures.immediateFuture(Collections.emptySet());
}
}
private PeerConnection createPeerConnection(
- final List iceServers, final boolean trickle) {
+ final Collection iceServers, final boolean trickle) {
final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle);
final PeerConnection peerConnection =
requirePeerConnectionFactory()
diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java
index 36338083da5d03530ee42027f618225e8c248632..b3d64f803c4aa18a1e84d8bf8c540f15e741aa3f 100644
--- a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java
+++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java
@@ -1,7 +1,19 @@
package im.conversations.android.xmpp.model.disco.external;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Ints;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.IP;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Arrays;
+import java.util.Collection;
+import org.webrtc.PeerConnection;
@XmlElement
public class Services extends Extension {
@@ -9,4 +21,104 @@ public class Services extends Extension {
public Services() {
super(Services.class);
}
+
+ public Collection getServices() {
+ return this.getExtensions(Service.class);
+ }
+
+ public Collection getIceServers() {
+ final var builder = new ImmutableSet.Builder();
+ for (final var service : this.getServices()) {
+ final String type = service.getAttribute("type");
+ final String host = service.getAttribute("host");
+ final String sport = service.getAttribute("port");
+ final Integer port = sport == null ? null : Ints.tryParse(sport);
+ final String transport = service.getAttribute("transport");
+ final String username = service.getAttribute("username");
+ final String password = service.getAttribute("password");
+ if (Strings.isNullOrEmpty(host) || port == null) {
+ continue;
+ }
+ if (port < 0 || port > 65535) {
+ continue;
+ }
+
+ if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type)
+ && Arrays.asList("udp", "tcp").contains(transport)) {
+ if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) {
+ Log.w(
+ Config.LOGTAG,
+ "skipping invalid combination of udp/tls in external services");
+ continue;
+ }
+
+ // STUN URLs do not support a query section since M110
+ final String uri;
+ if (Arrays.asList("stun", "stuns").contains(type)) {
+ uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
+ } else {
+ uri =
+ String.format(
+ "%s:%s:%s?transport=%s",
+ type, IP.wrapIPv6(host), port, transport);
+ }
+
+ final PeerConnection.IceServer.Builder iceServerBuilder =
+ PeerConnection.IceServer.builder(uri);
+ iceServerBuilder.setTlsCertPolicy(
+ PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK);
+ if (username != null && password != null) {
+ iceServerBuilder.setUsername(username);
+ iceServerBuilder.setPassword(password);
+ } else if (Arrays.asList("turn", "turns").contains(type)) {
+ // The WebRTC spec requires throwing an
+ // InvalidAccessError on empty username or password
+ // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
+ Log.w(
+ Config.LOGTAG,
+ "skipping "
+ + type
+ + "/"
+ + transport
+ + " without username and password");
+ continue;
+ }
+ final var iceServer = new IceServerWrapper(iceServerBuilder.createIceServer());
+ Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
+ builder.add(iceServer);
+ }
+ }
+ final var set = builder.build();
+ Log.d(Config.LOGTAG, "discovered " + set.size() + " ice servers");
+ return Collections2.transform(set, i -> i.iceServer);
+ }
+
+ private static class IceServerWrapper {
+
+ private final PeerConnection.IceServer iceServer;
+
+ private IceServerWrapper(final PeerConnection.IceServer iceServer) {
+ this.iceServer = iceServer;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof IceServerWrapper that)) return false;
+ return Objects.equal(iceServer.urls, that.iceServer.urls)
+ && Objects.equal(iceServer.username, that.iceServer.username)
+ && Objects.equal(iceServer.password, that.iceServer.password);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(iceServer.urls, iceServer.urls, iceServer.password);
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return this.iceServer.toString();
+ }
+ }
}
diff --git a/src/main/res/drawable/ic_folder_open_24dp.xml b/src/main/res/drawable/ic_folder_open_24dp.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9881c7748400cbf8037c92bfd11c0fea7454421c
--- /dev/null
+++ b/src/main/res/drawable/ic_folder_open_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/src/conversations/res/layout/activity_import_backup.xml b/src/main/res/layout/activity_import_backup.xml
similarity index 100%
rename from src/conversations/res/layout/activity_import_backup.xml
rename to src/main/res/layout/activity_import_backup.xml
diff --git a/src/cheogram/res/layout/dialog_enter_password.xml b/src/main/res/layout/dialog_enter_password.xml
similarity index 54%
rename from src/cheogram/res/layout/dialog_enter_password.xml
rename to src/main/res/layout/dialog_enter_password.xml
index 623168aa4f40a3c0994a995677d2a0f2f00f26d5..220ef0e6f808cab35d4e945376e6a3eab2ba01c2 100644
--- a/src/cheogram/res/layout/dialog_enter_password.xml
+++ b/src/main/res/layout/dialog_enter_password.xml
@@ -19,25 +19,40 @@
android:text="@string/enter_password_to_restore"
android:textAppearance="?textAppearanceBodyMedium" />
-
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml
index 1160b46e208ccc521bb6d7a76aa145724cf195ee..8422072914385d46ecb8e8d0a57a554a0166cc33 100644
--- a/src/main/res/values-ar/strings.xml
+++ b/src/main/res/values-ar/strings.xml
@@ -650,4 +650,4 @@
رمز التسجيل غير صالح
مفتاح تعمية خاطئ.
هذا ليس عنوان XMPP صالح
-
\ No newline at end of file
+
diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml
index 0f1a5262fe363969c1d08480d616e0db3de389e3..a2b32db6c25e50895478b1bc059971b4b78935e8 100644
--- a/src/main/res/values-bg/strings.xml
+++ b/src/main/res/values-bg/strings.xml
@@ -936,4 +936,4 @@
Създаването на резервно копие е стартирано. Ще получите известие, когато приключи.
Видеото не може да бъде включено.
Обикновен текстов документ
-
\ No newline at end of file
+
diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml
index ada5eff7f3395a63d0a4ca96c29a56a6e0e418d0..5a17d7fb7706dea1d75f3734311f74b493f60b25 100644
--- a/src/main/res/values-ca/strings.xml
+++ b/src/main/res/values-ca/strings.xml
@@ -914,4 +914,4 @@
No es pot processar la invitació
El servidor no admet la generació d\'invitacions
Cap compte actiu admet aquesta funció
-
\ No newline at end of file
+
diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml
index a4f80c370cb28b335ffd8706fa763ede8b4a950b..46c38fc2b291240d4304098133d22ee4f218a5fd 100644
--- a/src/main/res/values-cs/strings.xml
+++ b/src/main/res/values-cs/strings.xml
@@ -1132,4 +1132,4 @@
Integrace hovorů
Hovory z této aplikace interagují s běžnými telefonními hovory, například ukončení jednoho hovoru, když začne další.
Chcete smazat svůj avatar? Některé aplikace mohou i nadále zobrazovat uloženou kopii vašeho avataru.
-
\ No newline at end of file
+
diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml
index 3267d95d6325d050b3805d0d9d1ee65aa3dcfd4c..5ebae04597748751bd0c535f0bc7ff13c057311f 100644
--- a/src/main/res/values-da-rDK/strings.xml
+++ b/src/main/res/values-da-rDK/strings.xml
@@ -984,4 +984,4 @@
Udgående opkald (%s) · %s
Indkommende opkald (%s) · %s
Fjern konto fra server
-
\ No newline at end of file
+
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml
index 6fd5565ace53fc7da16f1d08e92a69eec50028c5..46a96d4ac0d9251c746ad2438d098d6572cc3176 100644
--- a/src/main/res/values-de/strings.xml
+++ b/src/main/res/values-de/strings.xml
@@ -309,8 +309,8 @@
Erneut senden
Datei-URL
URL in die Zwischenablage kopiert
- XMPP-Adresse in Zwischenablage kopiert
- Fehlermeldung in Zwischenablage kopiert
+ XMPP-Adresse in die Zwischenablage kopiert
+ Fehlermeldung in die Zwischenablage kopiert
Internetadresse
QR-Code scannen
QR-Code anzeigen
@@ -321,7 +321,7 @@
Vordergrunddienst
Verhindert, dass das Betriebssystem deine Verbindung unterbricht
Sicherung erstellen
- Sicherungsdateien werden gespeichert in %s
+ Sicherungen werden gespeichert in %s
Erstelle Sicherungsdateien
Deine Sicherung wurde erstellt
Die Sicherungsdateien wurden gespeichert in %s
@@ -819,12 +819,12 @@
E-Book
Original (unkomprimiert)
Öffnen mit…
- Conversations Profilbild
+ Profilbild
Konto auswählen
Sicherung wiederherstellen
Wiederherstellung
Gib dein Passwort für das Konto %s ein, um die Sicherung wiederherzustellen.
- Benutze die Sicherungsfunktion nicht, um eine Installation zu klonen (gleichzeitig auszuführen). Die Wiederherstellung einer Sicherung ist nur für Migrationen oder für den Fall gedacht, dass du das ursprüngliche Gerät verloren hast.
+ Stelle die OMEMO-Schlüssel nicht wieder her, um eine Installation zu klonen (gleichzeitig auszuführen). Die Wiederherstellung von OMEMO-Schlüsseln ist nur für Migrationen oder für den Fall gedacht, dass du das ursprüngliche Gerät verloren hast.
Sicherung konnte nicht wiederhergestellt werden.
Sicherung konnte nicht entschlüsselt werden. Ist das Passwort korrekt?
Sicherung & Wiederherstellung
@@ -987,7 +987,7 @@
Konto konnte nicht vom Server gelöscht werden
Gruppenchats
Gruppenchats durchsuchen
- Versuche nicht, Backups wiederherzustellen, die du nicht selbst erstellt hast!
+ Nur Sicherungen wiederherstellen, die du selbst erstellt hast.
Du versuchst, ein veraltetes Sicherungsdateiformat zu importieren
Hörbuch
Verbindung auf anderem Host wiederherstellen
@@ -1110,4 +1110,17 @@
Nur für Kontakte anzeigen
Zeitüberschreitung beim Verbinden
Erneut mit P2P versuchen
-
\ No newline at end of file
+ Kanalbindung nicht verfügbar
+ Word-Dokument
+ OMEMO-Schlüssel wiederherstellen
+ Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen
+ Sicherungsort
+ URI
+ Telefonnummer kopieren
+ Standort kopieren
+ E-Mail-Adresse kopieren
+ Telefonnummer in die Zwischenablage kopiert
+ URI in die Zwischenablage kopiert
+ URI kopieren
+ E-Mail-Adresse in die Zwischenablage kopiert
+
diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml
index 99f7207e06c9ef12a7dece19e02defcebf4bddc3..02131a13b0cec856d828e1a4bfb891e3d4ad3807 100644
--- a/src/main/res/values-el/strings.xml
+++ b/src/main/res/values-el/strings.xml
@@ -944,4 +944,4 @@
Έγγραφο απλού κειμένου
Δεν υποστηρίζονται εγγραφές λογαριασμών
Δεν βρέθηκε διεύθυνση XMPP
-
\ No newline at end of file
+
diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml
index 658f5f2d8a183d145252098360d07c82e606b164..1ace9452d402cc28813d1abcf47d3ae302c8692c 100644
--- a/src/main/res/values-es/strings.xml
+++ b/src/main/res/values-es/strings.xml
@@ -1,6 +1,6 @@
- Ajustes
+ Configuración
Gestionar cuentas
Gestionar cuenta
Detalles del contacto
@@ -16,10 +16,10 @@
Desbloquear dominio
Bloquear participante
Desbloquear participante
- Gestionar Cuentas
- Ajustes
- Seleccionar Contacto
- Seleccionar Contactos
+ Gestionar cuentas
+ Configuración
+ Elegir contacto
+ Elegir contactos
Compartir via cuenta
Lista contactos bloqueados
ahora
@@ -31,18 +31,18 @@
- %d chats sin leer
enviando…
- Descifrando el mensaje. Espere por favor…
+ Descifrando el mensaje. Espere…
Mensaje cifrado con OpenPGP
El apodo ya está en uso
- Apodo inválido
+ Apodo no válido
Administrador
Propietario
Moderador
Participante
Visitante
- ¿Quieres eliminar a %s de tu lista de contactos? Los chats con este contacto no se eliminarán.
- ¿Quieres bloquear a %s para que no pueda enviarte mensajes?
- ¿Quieres desbloquear a %s y permitirle que te envíe mensajes?
+ ¿Quiere eliminar a %s de su lista de contactos? La conversación con este contacto no se eliminará.
+ ¿Quiere bloquear a %s para que no pueda enviarle mensajes?
+ ¿Quiere desbloquear a %s y permitirle que le envíe mensajes?
¿Bloquear todos los contactos de %s?
¿Desbloquear todos los contatos de %s?
Contacto bloqueado
@@ -62,16 +62,16 @@
Bloquear
Desbloquear
Guardar
- OK
+ Aceptar
%1$s se ha detenido
Usar tu cuenta XMPP para enviar trazas de error ayuda al desarrollo de %1$s.
Enviar ahora
No preguntar de nuevo
No se ha podido conectar a la cuenta
No se ha podido conectar a varias cuentas
- Pulsa aquí para gestionar tus cuentas
+ Toque para gestionar sus cuentas
Adjuntar
- El contacto no está en tu lista. ¿Te gustaría añadirlo?
+ El contacto no está en su lista. ¿Le gustaría añadirlo?
Añadir contacto
Error al enviar
Preparando para enviar imagen
@@ -83,26 +83,24 @@
\n
\nAviso: Esto no afectará a los mensajes guardados en otros dispositivos o servidores.
Eliminar fichero
- ¿Está seguro de que desea eliminar este archivo\?
-\n
-\nAdvertencia: Esto no eliminará las copias de este archivo almacenadas en otros dispositivos o servidores.
- Seleccionar dispositivo
+ ¿Confirma que quiere eliminar este archivo? \n \nAviso: esto no eliminará las copias de este archivo almacenadas en otros dispositivos o servidores.
+ Elegir dispositivo
Enviar mensaje sin cifrar
Enviar mensaje
Enviar mensaje a %s
Enviar mensaje cifrado v\\OMEMO
El apodo ha sido modificado
Enviar sin cifrar
- Falló el descifrado. Tal vez no tengas la clave privada apropiada.
+ Falló el descifrado. Tal vez no tenga la clave privada apropiada.
OpenKeychain
OpenKeychain para cifrar y descifrar mensajes y gestionar tus claves públicas.
Está publicado bajo licencia GPLv3+ y disponible en F-Droid y Google Play.
(Por favor, reinicie %1$s después.)]]>
Reiniciar
Instalar
- Por favor, instala OpenKeyChain
+ Instale OpenKeyChain
ofreciendo…
esperando…
Clave OpenPGP no encontrada
- No se ha podido cifrar tu mensaje porque tu contacto no está anunciando su clave pública.\n\nPor favor, pide a tu contacto que configure OpenPGP.
+ No se ha podido cifrar tu mensaje porque su contacto no está anunciando su clave pública.\n\nPida a su contacto que configure OpenPGP.
Claves OpenPGP no encontradas
No se ha podido cifrar tu mensaje porque tus contactos no están anunciando sus claves públicas.\n\nPor favor, pide a tus contactos que configuren OpenPGP.
General
@@ -123,7 +121,7 @@
Avanzado
Al enviar los informes de los fallos, ayudará a un mayor desarrollo
Confirmar mensajes
- Permitir a tus contactos saber cuando has recibido y leído sus mensajes
+ Permitir a sus contactos saber cuando ha recibido y leído sus mensajes
Impedir capturas de pantalla
Ocultar el contenido de la aplicación en el selector de aplicaciones y bloquear las capturas de pantalla
Pantalla
@@ -132,11 +130,11 @@
Aceptar
Ha ocurrido un error
Error
- Tu cuenta
+ Su cuenta
Enviar actualizaciones de presencia
Recibir actualizaciones de presencia
Solicitar actualizaciones de presencia
- Seleccionar imagen
+ Elegir imagen
Hacer foto
De forma automática conceder suscripción de presencia
El archivo seleccionado no es una imagen
@@ -148,7 +146,7 @@
\nUse otro administrador de archivos para seleccionar una imagen.
La aplicación que utilizó para compartir este archivo no tiene suficientes permisos.
Desconocido
- Deshabilitado temporalmente
+ Desactivado temporalmente
Conectado
Conectando\u2026
Desconectado
@@ -158,7 +156,7 @@
Error en el registro
El identificador ya está en uso
Registro completado
- El servidor no soporta registros
+ El servidor no admite registros
Token de registro inválido
Error de negociación TLS
Dominio no verificable
@@ -172,14 +170,14 @@
OpenPGP
OMEMO
Eliminar cuenta
- Deshabilitar temporalmente
- Imagen de perfil
+ Desactivar temporalmente
+ Publicar avatar
Publicar clave pública OpenPGP
Eliminar la clave pública OpenPGP
¿Estás seguro de que quieres eliminar tu clave pública OpenPGP de tu anuncio de presencia?\nTus contactos no podrán enviarte mensajes cifrados con OpenPGP.
La clave pública OpenPGP ha sido publicada.
- Habilitar
- ¿Estás seguro de que desea eliminar tu cuenta? Eliminar tú cuenta borrando todo tu historial del chat
+ Activar cuenta
+ ¿Confirma que quiere eliminar su cuenta? Eliminar la cuenta borrará por completo el histórico de conversaciones
Grabar audio
Dirección XMPP
Bloquear dirección XMPP
@@ -187,7 +185,7 @@
Contraseña
Esta no es una dirección XMPP válida
Sin memoria. La imagen es demasiado grande
- ¿Quieres añadir a %s a tus contactos?
+ ¿Quiere añadir a %s a sus contactos?
Información de servidor
XEP-0313: MAM
XEP-0280: Copias de los mensajes
@@ -201,7 +199,7 @@
XEP-0357: Notificaciones automáticas
Sí
No
- Se han perdido las claves de anuncio públicas
+ Anuncios de clave pública no notificados
Visto última vez ahora
visto última vez hace un minuto
Visto última vez hace %d minutos
@@ -235,30 +233,30 @@
canal@salas.ejemplo.com
Guardar en marcadores
Eliminar marcador
- Destruir conversación en grupo
+ Destruir conversación grupal
Destruir canal
- ¿Estás seguro de que quieres destruir esta conversación en grupo?\n\nAviso:La conversación en grupo será eliminada completamente en el servidor.
- ¿Estás seguro de que quieres destruir este canal público?\n\nAviso:El canal será eliminado completamente en el servidor.
- No se ha podido destruir la conversación en grupo
+ ¿Confirma que quiere destruir esta conversación grupal?\n\nAviso: la conversación grupal se eliminará completamente del servidor.
+ ¿Confirma que quiere destruir este canal público?\n\nAviso: el canal se eliminará completamente del servidor.
+ No se ha podido destruir la conversación grupal
No se ha podido destruir el canal
Editar asunto de la conversación
Asunto
Uniéndose a un chat de grupo…
- Salir
- El contacto te ha añadido a su lista de contactos
- Añadir contacto
+ Abandonar
+ El contacto le ha añadido a su lista de contactos
+ Añadir de vuelta
%s ha leído hasta aquí
%s han leído hasta aquí
%1$s + %2$d han leído hasta aquí
Todos han leído hasta aquí
Publicar
- Pulsa la imagen de perfil para seleccionar una imagen de la galería
+ Toque el avatar para seleccionar una imagen de la galería
Publicando…
El servidor rechazó la publicación
No se ha podido convertir su imagen
No se ha podido guardar la imagen de perfil en disco
(O pulsación prolongada para volver a tu imagen de la agenda)
- Tu servidor no soporta la publicación de imágenes de perfil
+ Su servidor no admite la publicación de avatares
en privado
en privado para %s
Enviar mensaje privado a %s
@@ -267,14 +265,14 @@
Siguiente
Sesión establecida
Omitir
- Deshabilitar notificaciones
- Habilitar
+ Desactivar notificaciones
+ Activar
Esta conversación en grupo requiere contraseña
- Introduce la contraseña
+ Introduzca la contraseña
Por favor, solicita la actualización de presencia a tu contacto primero.\n\nEsto se usará para determinar qué aplicación de mensajería está usando tu contacto.
Solicitar ahora
Ignorar
- Aviso: Si envías esto sin actualización de presencia mutua con tu contacto se podrían producir problemas inesperados.\n\nVe a “Detalles del contacto” para verificar las actualizaciones de presencia.
+ Aviso: si envía esto sin actualización de presencia mutua con su contacto se podrían producir problemas inesperados.\n\nVaya a «Detalles del contacto» para verificar las actualizaciones de presencia.
Seguridad
Corrección de los mensajes
Permitir a tus contactos editar mensajes previamente enviados
@@ -283,7 +281,7 @@
Acerca de %s
Horario de silencio
Hora de comienzo
- Hora de fin
+ Hora de finalización
Habilitar horario de silencio
Las notificaciones serán silenciadas durante el horario de silencio
Otros
@@ -322,14 +320,14 @@
Servicio en primer plano
Mantener el servicio en primer plano previene que el sistema cierre la conexión
Crear una copia de respaldo
- Los ficheros de respaldo serán almacenados en %s
+ Las copias de seguridad se almacenarán en %s
Creando los ficheros de respaldo
Tu copia de respaldo ha sido creada
Los ficheros de respaldo han sido almacenados en %s
Restaurando copia de respaldo
Tu copia de respaldo ha sido restaurada
No olvides activar la cuenta.
- Seleccionar archivo
+ Elegir archivo
Recibiendo %1$s (%2$d%% completado)
Descargar %s
Eliminar %s
@@ -347,7 +345,7 @@
No se ha encontrado aplicación para ver el contacto
Etiquetas dinámicas
Mostrar información en forma de etiquetas debajo de los contactos
- Habilitar notificaciones
+ Activar notificaciones
No se ha encontrado el servidor de la conversación en grupo
No se ha podido crear la conversación en grupo
Imagen de perfil
@@ -367,8 +365,8 @@
Contraseña actual
Nueva contraseña
La contraseña no puede ser vacía
- Habilitar todas las cuentas
- Deshabilitar todas las cuentas
+ Activar todas las cuentas
+ Desactivar todas las cuentas
Realizar acción con
Sin afiliación
Desconectado
@@ -404,7 +402,7 @@
Marcar como leído
Entrada
Intro para enviar
- Utilizar la tecla Enter para enviar un mensaje. Siempre puedes usar Ctrl+Enter para enviar un mensaje, incluso si esta opción está deshabilitada.
+ Usar la tecla Intro para enviar un mensaje. Siempre puede usar Ctrl+Intro para enviar un mensaje, incluso si esta opción está desactivada.
Mostrar tecla Intro
Cambiar la tecla de emoticonos por la tecla Intro
audio
@@ -413,9 +411,9 @@
gráfico de vectores
archivo multimedia
documento PDF
- Android App
+ aplicación para Android
Contacto
- ¡La imagen de perfil ha sido publicada!
+ Se ha publicado el avatar.
Enviando %s
Ofreciendo %s
Ocultar desconectados
@@ -443,7 +441,7 @@
- %d certificados eliminados
- %d certificados eliminados
- Cambiar el botón de “Enviar” por el botón de acción rápida
+ Cambiar el botón «Enviar» por el botón de acción rápida
Acción rápida
Ninguna
Usada más recientemente
@@ -486,7 +484,7 @@
Renovar certificado
¡Error buscando clave OMEMO!
¡Clave OMEMO con certificado verificada!
- ¡Tu dispositivo no soporta la elección de certificados de cliente!
+ Su dispositivo no admite la selección de certificados de cliente.
Conexión
Conectar via Tor
Todas las conexiones se realizan a través de la red TOR. Requiere Orbot
@@ -514,23 +512,23 @@
\n¡Ningún dato de la lista de contactos sale de tu dispositivo!
Notificar para todos los mensajes
Notificar solo cuando eres mencionado
- Notificaciones deshabilitadas
+ Notificaciones desactivadas
Notificaciones pausadas
Compresión de imagen
- Pista: Usa \'Seleccionar archivo\' en lugar de \'Seleccionar imagen\' para enviar imágenes individuales sin comprimir con independencia de los ajustes.
+ Consejo: use «Elegir archivo» en lugar de «Elegir imagen» para enviar imágenes separadas no comprimidas sin tener en cuenta esta opción.
Siempre
Solo imágenes de gran tamaño
- Optimizaciones de uso de batería habilitadas
+ Optimizaciones de batería activadas
Tu dispositivo está empleando severas optimizaciones del uso de batería por parte de %1$s, lo cual puede hacer que las notificaciones se retrasen o incluso que los mensajes se pierdan.\nEs recomendable deshabilitarlas.
Tu dispositivo está empleando severas optimizaciones del uso de batería por parte de %1$s, lo cual puede hacer que las notificaciones se retrasen o incluso que los mensajes se pierdan.\n\nA continuación se te preguntará si quiere deshabilitarlas.
- Deshabilitar
+ Desactivar
El área seleccionada es demasiado grande
(No hay cuentas activas)
Este campo es requerido
Corregir mensaje
Enviar mensaje corregido
- Ya has confiado en la huella digital de esta persona. Al seleccionar “Hecho” solo estás confirmando que %s es parte de este chat grupal.
- Has deshabilitado esta cuenta
+ Ya ha confiado en la huella digital de esta persona. Al seleccionar «Hecho» solo está confirmando que %s es parte de este chat grupal.
+ Ha desactivado esta cuenta
Error de seguridad: ¡Acceso a archivo inválido!
No se ha encontrado ninguna aplicación para compartir la URI
Compartir URI con…
@@ -550,13 +548,13 @@
No disponible
Ocupado
Se ha generado una contraseña segura
- Tu dispositivo no soporta la opción de optimización de batería
+ Su dispositivo no admite desactivar la optimización de batería
El registro falló. Prueba de nuevo más tarde
Error en el registro: La contraseña es demasiado débil
Elige a los participantes
- Creando un chat de grupo…
+ Creando una conversación grupal…
Invitar de nuevo
- Deshabilitar
+ Desactivar
Corto
Medio
Largo
@@ -593,7 +591,7 @@
Mensaje de error
Optimización de datos habilitado
Tu sistema operativo está restringiendo a %1$s el acceso a Internet cuando está en segundo plano. Para recibir notificaciones de nuevos mensajes deberías permitir a %1$s un acceso sin restricciones cuando la optimización de datos está habilitada.\n%1$s se esforzará igualmente en ahorrar datos cuando sea posible.
- Tu dispositivo no soporta la opción de deshabilitar la optimización de datos para %1$s.
+ Su dispositivo no admite la desactivación de la optimización de datos para %1$s.
No se ha podido crear el archivo temporal
Este dispositivo ha sido verificado
Copiar huella digital
@@ -682,33 +680,33 @@
Copiar al portapapeles
Mensaje copiado en el portapapeles
Mensaje
- Los mensajes privados están deshabilitados
+ Los mensajes privados están desactivados
¿Aceptar certificado desconocido?
El certificado del servidor no está firmado por una Autoridad Certificadora conocida.
¿Aceptar nombre del servidor no coincidente?
- El servidor no pudo autenticarse como \"%s\". El certificado es solo válido para:
+ El servidor no pudo autenticarse como «%s». El certificado solo es válido para:
¿Quieres conectar de todas formas?
- Detalles del Certificado:
+ Detalles del certificado:
Una vez
El escáner de código QR necesita acceso a la cámara
Desplazarse hasta abajo
Desplazarse hasta abajo después de mandar un mensaje
- Editar Mensaje de Estado
+ Editar mensaje de estado
Editar mensaje de estado
- Deshabilitar cifrado
+ Desactivar cifrado
%1$s no puede enviar mensajes cifrados a %2$s. Esto puede deberse a que tu contacto está usando un servidor o un cliente desactualizado que no puede manejar las claves OMEMO.
No se ha podido conseguir la lista de dispositivos
No se han podido conseguir las claves de cifrado
Consejo: En algunas ocasiones esto puede corregirse agregando a tu contacto a tu lista de contactos. Tu contacto deberá asegurarse también que estás en su lista de contactos.
¿Estás seguro de que deseas desactivar el cifrado OMEMO para este chat?
\nEsto permitirá que el administrador de su servidor lea sus mensajes, pero podría ser la única forma de comunicarse con personas que utilizan clientes obsoletos.
- Deshabilitar ahora
+ Desactivar ahora
Borrador:
Cifrado OMEMO
- OMEMO siempre será usado para conversaciones uno a uno y en conversaciones en grupo privadas.
+ OMEMO siempre se usará para conversaciones uno a uno y en conversaciones grupales privadas.
OMEMO será usado por defecto para chats nuevos.
OMEMO tendrá que ser activado explícitamente para los nuevos chats.
- Crear acceso directo
+ Crear atajo
Activo por defecto
Desactivado por defecto
El mensaje no fue cifrado para este dispositivo.
@@ -718,7 +716,7 @@
Fijar posición
Desfijar posición
Copiar ubicación
- Compatir Ubicación
+ Compatir ubicación
Direcciones
Compartir ubicación
Mostrar ubicación
@@ -733,18 +731,18 @@
Usar el Plugin Compartir Ubicación en lugar del propio de la aplicación
Copiar dirección web
Copiar dirección XMPP
- Compartición de Archivos mediante S3
+ Compartición de archivos HTTP con S3
Búsqueda directa
- En la pantalla \"Nuevo chat\", abra el teclado y coloque el cursor en el campo de búsqueda
- Avatar de la conversación en grupo
- El servidor no soporta avatares en conversaciones en grupo
+ En la pantalla «Chat nuevo», abra el teclado y coloque el cursor en el campo de búsqueda
+ Avatar de la conversación grupal
+ El anfitrión no admite avatares en conversaciones grupales
Solo el propietario de la conversación puede cambiar el avatar
Nombre del contacto
Apodo
Nombre
Añadir un nombre es opcional
- Nombre de la Conversación en grupo
- Esta conversación en grupo ha sido destruida
+ Nombre de la conversación grupal
+ Esta conversación grupal se ha destruido
No se ha podido guardar la grabación
Servicio en primer plano
Esta categoría de notificación se usa para mostrar una notificación permantente indicando que %1$s está ejecutándose.
@@ -760,8 +758,8 @@
Mensajes sin sonido
Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia).
Envíos fallidos
- Ajustes de notificación de mensajes
- Ajustes de notificación de llamadas
+ Configuración de notificaciones de mensajes
+ Configuración de notificaciones de llamadas
Importancia, Sonido, Vibración
Compresión de video
Ver galería
@@ -794,7 +792,7 @@
Atrás
Pegado automático del posible PIN desde el portapapeles.
Por favor, introduzca su PIN de 6 dígitos.
- ¿Estás seguro de que quieres abortar el proceso de registro?
+ ¿Confirma que quiere interrumpir el proceso de registro?
Sí
No
Verificando…
@@ -828,19 +826,19 @@
e-book
Original (sin comprimir)
Abrir con…
- Foto de perfil de Conversations
+ Avatar
Elige una cuenta
Restaurar copia de respaldo
Restaurar
Introduce tu contraseña para la cuenta %s para restaurar la copia de respaldo.
- No utilices la opción de restaurar una copia de respaldo para clonar (ejecutar simultáneamente) una instalación. Restaurar una copia de respaldo se debe utilizar solo para migraciones o en caso de que hayas perdido el dispositivo original.
+ No restaures claves OMEMO en un intento de clonar (ejecutar simultáneamente) una instalación. La restauración de claves OMEMO solo está pensada para migraciones o en caso de que hayas perdido el dispositivo original.
No se ha podido restaurar la copia de respaldo.
No se ha podido descifrar la copia de respaldo. ¿Es la contraseña correcta?
Respaldar & Restaurar
Introduce dirección XMPP
- Crear una conversación en grupo
+ Crear una conversación grupal
Unirse a canal público
- Crear una conversación en grupo privada
+ Crear una conversación grupal privada
Crear un canal público
Nombre del canal
Dirección XMPP
@@ -892,7 +890,7 @@
Método para la búsqueda de Canales
Copia de respaldo
Acerca de
- Por favor, habilita una cuenta
+ Active una cuenta
Hacer una llamada
Llamada entrante
Videollamada entrante
@@ -918,7 +916,7 @@
Video llamada saliente
Reconectando llamada
Reconectando video llamada
- Deshabilitar Tor para hacer llamadas
+ Desactive Tor para hacer llamadas
Llamada entrante
Llamada perdida · %s
Llamada saliente
@@ -976,16 +974,16 @@
No se ha encontrado aplicación
Invitar a Conversations
No se ha podido leer la invitación
- El servidor no soporta la creación de invitaciones
- Ninguna cuenta activa soporta esta característica
+ El servidor no admite la generación de invitaciones
+ Ninguna cuenta activa admite esta funcionalidad
La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado.
- No se ha podido habilitar el vídeo.
+ No se ha podido activar el vídeo.
Documento de texto plano
- Los registros de cuenta no están soportados
+ No se admiten las altas de cuentas
Dirección XMPP no encontrada
Fallo temporal de autenticación
Eliminar imagen de perfil
- Las llamadas están deshabilitadas cuando se usa Tor
+ Las llamadas se desactivan cuando se usa Tor
Cambiar a vídeo
Rechazar petición de cambiar a vídeo
Distribuidor de UnifiedPush
@@ -999,9 +997,9 @@
Rechazar
Eliminar la cuenta del servidor
No se pudo eliminar la cuenta del servidor
- Chats en grupo
+ Conversaciones grupales
Buscar un grupo de chats
- ¡No intentes restaurar las copias de seguridad que no creaste tu mismo!
+ Restaura solo las copias de seguridad que hayas creado personalmente.
Estás intentando importar un formato de copia de seguridad obsoleto
Audiolibro
Reconectarse a otros hosts
@@ -1011,12 +1009,12 @@
Tu contacto utiliza dispositivos no verificados. Escanea su código QR para realizar la verificación e impedir ataques MITM activos.
Desconectarse
Desconectado
- Estás utilizando dispositivos no verificados. Escanea el código QR de tus otros dispositivos para realizar la verificación e impedir ataques MITM activos.
+ Está utilizando dispositivos no verificados. Escanee el código QR en sus otros dispositivos para verificarlos e impedir ataques MITM activos.
Informar de spam y bloquear al spammer
Informar sobre spam
¡Bienvenido a Quicksy!
Quicksy pide tu consentimiento para utilizar tus datos
- Política de privacidad
+ Normativa de privacidad
La lista de contactos no está disponible
Sin permiso para llamar por teléfono
Contacto no disponible
@@ -1044,11 +1042,11 @@
Al actuar como un Distribuidor de UnifiedPush la conexión XMPP persistente, fiable y de bajo consumo de batería se utilizará para despertar a otras aplicaciones compatibles con UnifiedPush como Tusky, Ltt.rs, FluffyChat y más.
Enviar mensaje cifrado
Interfaz
- Tema, Colores, Capturas de pantalla, Entrada
+ Tema, colores, capturas de pantalla, entrada
Seguridad
Relé de notificaciones para aplicaciones de terceros compatibles con UnifiedPush
Notificaciones
- Período de gracia, Tono de llamada, Vibración, Extraños
+ Período de gracia, tono de llamada, vibración, extraños
Enviando
Recibiendo
Descarga automática
@@ -1073,19 +1071,19 @@
Iniciar un chat
El descubrimiento de canales utiliza un servicio de terceros llamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Usar esta función transmitirá tu dirección IP y términos de búsqueda a ese servicio. Consulte su <a href=https://search.jabber.network/privacy>Política de privacidad</a> para obtener más información.
¡No se ha seleccionado ningún certificado de cliente!
- Tamaño de archivo, Compresión de imagen, Calidad de vídeo
+ Tamaño de archivo, compresión de imagen, calidad de vídeo
Mostrar el contenido de la aplicación en el conmutador de aplicaciones y permitir la realización de capturas de pantalla
Invitaciones de extraños
Aceptar invitaciones a chats grupales de extraños
Crear una sola vez, Programar recurrentes
Crear una copia de seguridad única
Copia de seguridad periódica
- Operación no soportada
+ Operación no admitida
Notificaciones a pantalla completa
Permite que esta aplicación muestre notificaciones de llamadas entrantes que ocupan toda la pantalla cuando el dispositivo está bloqueado.
Permitir mensajes privados
Más reacciones
- Tu avatar. Toca para seleccionar un nuevo avatar de la galería.
+ Su avatar. Toque para seleccionar un avatar nuevo de la galería.
XEP-0386: Vinculación 2
XEP-0388: Perfil SASL Extensible
No se pudo desactivar el video.
@@ -1096,32 +1094,37 @@
Editar nombre y tema
Cambiar configuración
Cambiar la configuración de notificaciones
- La llamada está utilizando el auricular. Toca para cambiar al altavoz.
+ La llamada está utilizando el auricular. Toque para cambiar al altavoz.
La llamada está utilizando el auricular.
La llamada está utilizando auriculares con cable
- La llamada está utilizando altavoz. Toca para cambiar a auricular.
+ La llamada está utilizando el altavoz. Toque para cambiar al auricular.
La llamada está usando el altavoz.
- La llamada está usando bluetooth.
+ La llamada está usando Bluetooth.
Video desactivado. Toca para activar.
Método de acceso
- No se pudo agregar la reacción
- Agregar reacción…
- Agregar reacción
- El cliente XMPP de tu contacto puede que no soporte llamadas de audio/video.
+ No se pudo añadir la reacción
+ Añadir reacción…
+ Añadir reacción
+ El cliente XMPP de su contacto puede que no admita llamadas de audio/vídeo.
No se pudo modificar la llamada
Burbujas de chat
Color, Tamaño de fuente, Imágenes de perfil
Burbujas de Chat
Integración de llamadas
- Ver imágenes de perfil
+ Mostrar avatares
Mostrar imágenes de perfil para tus mensajes y conversaciones 1:1, aparte de las conversaciones en grupo.
Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como ser finalizar una llamada cuando recibimos otra.
Mensajes alineados a la izquierda
Mostrar todos los mensajes, incluso los propios, sobre el margen izquierdo para una distribución uniforme del chat.
Notificaciones personalizadas
- ¿Habilitar los ajustes de notificaciones personalizadas (importancia, sonido, vibración) para esta conversación?
+ ¿Quiere activar la configuración de notificaciones personalizadas (importancia, sonido, vibración) para esta conversación?
¿Quieres eliminar tu imagen de perfil? Algunos clientes podrían seguir mostrando una copia en caché de tu avatar.
Mostrar sólo a contactos
Se agotó el tiempo de espera de la conexión
Reintentar con P2P
-
\ No newline at end of file
+ documento de Word
+ Quicksy solo puede restaurar las copias de seguridad de las cuentas quicksy.im
+ Ubicación de la copia de seguridad
+ Restaurar claves OMEMO
+ Canal no disponible
+
diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml
index c1e8c9b859b921f50df527d62d23cd2ffe68615c..a3e1991a94eff04be51677690a1e2dbb199b5873 100644
--- a/src/main/res/values-et/strings.xml
+++ b/src/main/res/values-et/strings.xml
@@ -341,7 +341,7 @@
Osalejad
Vaid omanik võib muuta vestlusrühma profiilipilte
Kontakti nimi
- Conversationsi profiilipilt
+ Tunnuspilt
Varukoopiast taastamiseks sisesta %s kasutajakonto salasõna.
Selleks, et need, kellel pole sind oma aadressiraamatus, saaksid teada, kes sa oled, siis palun lisa oma nimi.
See kanal teeb sinu XMPP-aadressi avalikuks
@@ -441,7 +441,7 @@
Teenus esiplaanil
See eelistus takistab operatsioonisüsteemil sinu võrguühenduse sulgemist
Tee varukoopia
- Varukoopia failide salvestamise asukoht: %s
+ Varukoopiate salvestamise asukoht: %s
Sinu varukoopia on tehtud
Teeme varukoopiat
Varukoopia failid on salvestatud siin kaustas: %s
@@ -889,8 +889,8 @@
Palun oota %s ja proovi uuesti
Sinu tehtavatele päringutele kehtib hetkel ajaühikuline piirang
Liiga palju päringuid
- Ära kasuta varukoopiat olemasoleva paigalduse kloonimiseks (samaaegseks käivitamiseks). Varukoopiast taastamine on mõeldud vaid teise seadmesse kolimise jaoks ning juhuks, kui kaotad oma algse nutiseadme.
- Palun ära kasuta varukoopiaid, mida sa pole ise teinud!
+ Ära kasuta OMEMO võtmete taastamist olemasoleva paigalduse kloonimiseks (samaaegseks käivitamiseks). OMEMO võtmete taastamine on mõeldud vaid teise seadmesse kolimise jaoks ning juhuks, kui kaotad oma algse nutiseadme.
+ Palun taasta varukoopiatest, mille sa oled ise teinud.
Varukoopiast taastamine ei õnnestunud.
Kohalik server
Enamus kasutajaid peaksid eelistama valikut „jabber.network“. See tagab asjakohasemad soovitused kogu avalikust XMPP võrgustikust.
@@ -1131,4 +1131,16 @@
Ühenduse on aegunud
Proovi uuesti võrdõigusvõrguga
Edastuskanaliga sidumine pole võimalik
-
\ No newline at end of file
+ Wordi-dokument
+ Taasta OMEMO võtmed
+ Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid
+ Varukoopia asukoht
+ URI
+ Kopeeri URI
+ Kopeeri geograafilised koordinaadid
+ Kopeeri e-posti aadress
+ Kopeerisime e-posti aadressi lõikelauale
+ Kopeerisime URI lõikelauale
+ Kopeeri telefoninumber
+ Kopeerisime telefoninumbri lõikelauale
+
diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml
index 7b03bfb16bac0b1c976fb66a8aa318374e31692a..3c477fc665dcd71e322586ff973d7c886d3f6fa0 100644
--- a/src/main/res/values-eu/strings.xml
+++ b/src/main/res/values-eu/strings.xml
@@ -732,4 +732,4 @@
- Parte-hartzaile %1$d ikusi
- %1$d parte-hartzaile ikusi
-
\ No newline at end of file
+
diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml
index b3f8bd568ad6748f0ea6baaa73d9d3f52195bfb1..90295e1b399dc24a798104964bdeafa6f6796343 100644
--- a/src/main/res/values-fa-rIR/strings.xml
+++ b/src/main/res/values-fa-rIR/strings.xml
@@ -1027,4 +1027,4 @@
این برنامه برای بهکاربردن دادههای شما نیازمند موافقت شماست
یکپارچهسازی تماس تلفنی در دسترس نیست!
اجازهٔ تماس تلفنی وجود ندارد
-
\ No newline at end of file
+
diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml
index 16021812d83ca40f4fc1f9a01eb7b3b59f9a4401..70d97e9cf21262120d48a70cc75492bd19b3a40c 100644
--- a/src/main/res/values-fr/strings.xml
+++ b/src/main/res/values-fr/strings.xml
@@ -66,10 +66,10 @@
%1$s a planté
En utilisant votre compte XMPP pour envoyer des rapports de crash, vous aidez le développement de %1$s.
Envoyer
- Ne plus me demander
- Impossible de se connecter au compte.
- Impossible de se connecter aux comptes.
- Appuyez pour gérer vos comptes.
+ Ne plus demander
+ Impossible de se connecter au compte
+ Impossible de se connecter à plusieurs comptes
+ Appuyez pour gérer vos comptes
Joindre un fichier
Ajouter ce contact manquant à votre liste de contact ?
Ajouter un contact
@@ -87,15 +87,15 @@
\n
\nAvertissement : Cela ne supprimera pas les copies de ce fichier qui sont stockées sur d\'autres appareils ou serveurs.
Choisir l\'appareil
- Envoyer un message en clair
+ Envoyer un message non-chiffré
Envoyer le message
Envoyer un message à %s
- Envoyer un message chiffré avec \\OMEMO
- Votre identifiant a été changé
- Envoyer en clair
- Échec du déchiffrement. Avez-vous la bonne clé privée ?
+ Envoyer un message chiffré avec v\\OMEMO
+ Votre pseudo a été changé
+ Envoyer non-chiffré
+ Échec du déchiffrement. Peut-être que vous n\'avez pas la bonne clé privée.
OpenKeychain
- %1$s utilise <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.<br><br>OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.<br><br><small>(Veuillez redémarrer %1$s après l\'installation de l\'application.)</small>
+ OpenKeychain pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.
OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.
(Veuillez redémarrer %1$s après l\'installation de l\'application.)]]>
Redémarrer
Installer
Veuillez installer OpenKeychain
@@ -132,9 +132,9 @@
Une erreur s\'est produite
Erreur
Votre compte
- Envoyer mes màj de disponibilité
- Recevoir ses màj de disponibilité
- Demander les màj de disponibilité
+ Envoyer mes màj de statut
+ Recevoir ses màj de statut
+ Demander les màj de statut
Choisir une image
Prendre une photo
Accepter par avance les demandes de publication
@@ -169,14 +169,14 @@
OTR
OpenPGP
OMEMO
- Supprimer
+ Supprimer le compte
Désactiver temporairement
Publier une image de profil
Publier la clé publique OpenPGP
Supprimer la clé publique OpenPGP
Êtes-vous sûr de vouloir supprimer votre clé publique OpenPGP de votre annonce de présence ?\nVos contacts ne pourront plus vous envoyer de message chiffrés avec OpenPGP.
Clé publique OpenPGP publiée.
- Activer le compter
+ Activer le compte
Êtes-vous sûr de vouloir supprimer votre compte ? Supprimer votre compte effacera l\'historique de vos conversations
Enregistrer un son
Adresse XMPP
@@ -191,10 +191,10 @@
XEP-0280 : Copies carbone
XEP-0352 : Indication statut client
XEP-0191 : Commande de blocage
- XEP-0237 : Révision contacts
+ XEP-0237 : Versioning des contacts
XEP-0198 : Gestion des flux
XEP-0215 : Découverte de service externe
- XEP-0163 : PEP (Avatars / OMEMO)
+ XEP-0163 : PEP (Image de profil / OMEMO)
XEP-0363 : Envoi de fichiers via HTTP
XEP-0357 : Notifications Push
supportée
@@ -224,11 +224,11 @@
Afficher les détails du contact
Bloquer le contact
Débloquer le contact
- Ajouter
+ Créer
Sélectionner
Le contact existe déjà
Rejoindre
- salon@conference.example.com/surnom
+ salon@conference.example.com/pseudo
salon@conference.example.com
Enregistrer comme favori
Supprimer le favori
@@ -244,14 +244,14 @@
Sujet
Rejoindre le groupe…
Partir
- Votre correspondant vous a ajouté dans sa liste de contacts
+ Votre correspondant·e vous a ajouté dans sa liste de contacts
Ajouter en retour
%s a tout lu jusqu\'ici
%s ont tout lu jusqu\'ici
- %1$s+%2$d autres ont tout lu jusqu\'ici
+ %1$s +%2$d autres ont tout lu jusqu\'ici
Tout le monde a lu jusqu\'ici
Publier
- Appuyer sur l\'image de profil pour choisir une image depuis la galerie
+ Appuyer sur l\'image de profil pour en choisir une depuis la galerie
Mise à jour…
Le serveur a rejeté votre publication
Impossible de convertir votre image
@@ -304,7 +304,7 @@
Options du message
Citation
Coller en tant que citation
- Copier l\'URL
+ Copier l\'URL originale
Envoyer de nouveau
URL du fichier
URL copiée dans le presse-papier
@@ -361,7 +361,7 @@
Mise à jour…
Mot de passe modifié !
Impossible de changer le mot de passe
- Changer de mot de passe
+ Changer le mot de passe
Mot de passe actuel
Nouveau mot de passe
Le mot de passe ne peut pas être vide
@@ -370,7 +370,7 @@
Faire une action avec
Aucune affiliation
Hors ligne
- Banni
+ Banni·e
Membre
Mode avancé
Accorder des privilèges aux membres
@@ -404,7 +404,7 @@
Touche Entrée pour envoyer
Utilisez la touche Entrée pour envoyer un message. Vous pourrez toujours utiliser la combinaison Ctrl+Entrée pour envoyer un message, même si cette option est désactivée.
Afficher la touche Entrée
- Remplacer la touche Émoticônes par la touche Entrée
+ Remplacer la touche Emoji par la touche Entrée
audio
vidéo
image
@@ -427,7 +427,7 @@
Position
Quitter le groupe privé
Quitte le salon public
- Ne pas utiliser les CAs système
+ Ne pas utiliser les AC du système
Tous les certificats doivent être approuvés manuellement
Retirer les certificats
Supprimer les certificats approuvés manuellement
@@ -457,7 +457,7 @@
Échec du téléchargement : Fichier non valide
Réseau Tor inaccessible
La liaison a échoué
- Le serveur n\'est pas responsable pour ce domaine
+ Pas responsable pour le domaine
Détraqué
Disponibilité
Absent quand l\'appareil est verrouillé
@@ -772,7 +772,7 @@
Numéro de téléphone
Vérifier votre numéro de téléphone
Quicksy va envoyer un message SMS (des frais opérateurs sont possibles) pour vérifier votre numéro de téléphone. Saisissez votre code de pays et votre numéro de téléphone :
-
%s
. Est-ce correct ou souhaitez-vous modifier le numéro ?]]>
+
%s
. Est-ce correct ou souhaitez-vous modifier le numéro ?]]>
%s n\'est pas un numéro de téléphone valide.
Veuillez saisir votre numéro de téléphone.
Recherche de pays
@@ -877,7 +877,7 @@
Impossible de réaliser cette action
Rejoindre le salon public…
L\'application de partage n\'a pas accordé la permission d\'accéder à ce fichier.
- Salons et groupes de discussion
+
jabber.network
Serveur local
La plupart des utilisateur·ices devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP.
@@ -1099,7 +1099,7 @@
Modifier les paramètres de notification
L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur.
L\'appel passe par les écouteurs.
- XEP-0386 : Bind 2
+ XEP-0386: Bind 2
Éditer le pseudo
Supprimer la clé OpenPGP
L\'appel passe par le bluetooth.
@@ -1110,4 +1110,27 @@
L\'appel passe par le haut-parleur. Tapotez pour passer sur les écouteurs.
L\'appel passe par le haut-parleur.
Mécanisme de connexion
+ Délai d\'attente de connexion dépassé
+ Ajouter une réaction…
+ Couleur de fond, taille de police, images de profil
+ Document Word
+ Montrer les images de profil pour vos messages et dans les discussions un-à-un, en plus des groupes de discussion.
+ Voulez-vous supprimer votre image de profil ? Certains clients pourraient continuer d\'en afficher une version mise en cache.
+ Réessayer avec P2P
+ Bulles de discussion
+ Montrer aux contacts uniquement
+ Bulles de discussion
+ Impossible de modifier l\'appel
+ Channel binding indisponible
+ Le client XMPP de votre contact peut ne pas prendre en charge les appels audio/vidéo.
+ Impossible d\'ajouter une réaction
+ Intégration d\'appel
+ Messages alignés à gauche
+ Afficher tous les messages, y compris ceux envoyés, à gauche pour un rendu uniforme.
+ Notifications personnalisées
+ Activer les paramètres de notifications personnalisées (importance, son, vibration) pour cette discussion ?
+ Les appels de cette application intéragissent avec les appels téléphoniques normaux, comme en coupant un appel quand un autre commence.
+ Plus de réactions
+ Ajouter une réaction
+ Montrer l\'image de profil
diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml
index ed57de38b653b1eabaeb7a73e7a7c8cc051a7a88..c0eb867b6e549f8027a9d527e522484a3afab884 100644
--- a/src/main/res/values-gl/strings.xml
+++ b/src/main/res/values-gl/strings.xml
@@ -321,7 +321,7 @@
Servizo en primeiro plano
Evita que o sistema operativo corte a conexión
Crear copia de apoio
- Os ficheiros de copia gardaranse en %s
+ As copias vanse gardar en %s
Creando ficheiros de apoio
Creouse o ficheiro
Os ficheiros de apoio gardáronse en %s
@@ -819,12 +819,12 @@
e-book
Orixinal (non comprimido)
Abrir con…
- Imaxe de perfil en Conversations
+ Avatar
Elixir conta
Restablecer copia de apoio
Restablecer
Escribe o contrasinal da conta %s para restablecer a copia.
- Non utilices a función de restaurar a copia nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar unha copia só ten sentido para migrar ou en caso de perda do dispositivo orixinal.
+ Non restaures as claves OMEMO nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar as claves OMEMO só ten sentido para migrar ou en caso de perda do dispositivo orixinal.
Non se puido restaurar a copia.
Non se puido descifrar a copia. É correcto o contrasinal?
Respaldar & Restaurar
@@ -987,7 +987,7 @@
Non se puido eliminar a conta no servidor
Buscar parolas en grupo
Parolas en grupo
- Non intentes restablecer unha copia de apoio que non tiveses creado ti!
+ Restaura só copias de apoio que crearas ti persoalmente.
Estás intentando importar un ficheiro de apoio co formato antigo
Audiolibro
Volver conectar noutro servidor
@@ -1109,4 +1109,16 @@
Caducidade da conexión
Reintentar con P2P
Non está dispoñible a vinculación de canles
+ Documento de Word
+ Restaurar claves OMEMO
+ Quicksy só pode restaurar copias de apoio de contas quicksy.im
+ Localización das copias
+ URI copiado ao portapapeis
+ URI
+ Copiar URI
+ Copiar xeolocalización
+ Copiar enderezo de correo
+ Copiouse o enderezo ao portapapeis
+ Copiouse o número ao portapapeis
+ Copiar número de teléfono
diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml
index 6733e75a27d44f09b7591f5dbdac9c4d5b13363b..8b61ed745d75e2e28b6e091120d598f5836682be 100644
--- a/src/main/res/values-hu/strings.xml
+++ b/src/main/res/values-hu/strings.xml
@@ -1097,4 +1097,4 @@
Csevegőbuborékok
Háttérszín, betűméret, avatárok
Csevegőbuborékok
-
\ No newline at end of file
+
diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml
index 8fd32628050a99b914f0fa34f8e1e70441382eb9..16b6ab076e80a3b9a7c5fdb97e9f463024da7051 100644
--- a/src/main/res/values-id/strings.xml
+++ b/src/main/res/values-id/strings.xml
@@ -472,4 +472,4 @@
alamat XMPP
Buat channel publik...
Sibuk
-
\ No newline at end of file
+
diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml
index 73556f89d69479119f95a66422e19c1a83eeee30..f68233b5c618429cf6475df11002b507b6c2b114 100644
--- a/src/main/res/values-it/strings.xml
+++ b/src/main/res/values-it/strings.xml
@@ -322,7 +322,7 @@
Servizio in primo piano
Evita che il sistema operativo chiuda la connessione
Crea un backup
- I file di backup verranno salvati in %s
+ I backup verranno salvati in %s
Creazione dei file di backup
Il tuo backup è stato creato
I file di backup sono stati salvati in %s
@@ -828,12 +828,12 @@
e-book
Originale (non compresso)
Apri con…
- Immagine profilo di Conversations
+ Avatar
Scegli un profilo
Ripristina backup
Ripristina
Inserisci la tua password per il profilo %s per ripristinare il backup.
- Non usare la funzione di ripristino del backup tentando di clonare (eseguire simultaneamente) un\'installazione. Il ripristino di un backup è inteso solo per migrazioni o in caso di smarrimento del dispositivo.
+ Non ripristinare le chiavi OMEMO nel tentativo di clonare (eseguire simultaneamente) un\'installazione. Il ripristino delle chiavi OMEMO è inteso solo per migrazioni o in caso di smarrimento del dispositivo.
Impossibile ripristinare il backup.
Impossibile decifrare il backup. La password è giusta?
Backup e ripristino
@@ -1001,7 +1001,7 @@
Impossibile eliminare il profilo dal server
Chat di gruppo
Cerca chat di gruppo
- Non tentare di ripristinare dei backup che non hai creato te stesso!
+ Ripristina solo i backup che hai creato personalmente.
Stai tentando di importare un formato di file di backup obsoleto
Audiolibro
Riconnetti su altro host
@@ -1125,4 +1125,8 @@
Colore di sfondo, dimensione caratteri, avatar
Messaggi di chat
Associazione dei canali non disponibile
+ Documento Word
+ Ripristina chiavi OMEMO
+ Quicksy può ripristinare backup solo per profili quicksy.im
+ Percorso backup
diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml
index 7dcb57e0328b761d989fd6417ec5946f3a71055f..59bb5d68fd95cf67de384d3434f5eb02b1967276 100644
--- a/src/main/res/values-iw/strings.xml
+++ b/src/main/res/values-iw/strings.xml
@@ -52,7 +52,7 @@
נקה היסטוריה
נקה היסטוריית שיחה
שלח הודעה בלתי מוצפנת
- שלח ללא הצפנה
+ שלח טקסט נקי
פענוח נכשל. אולי אין לך את המפתח הפרטי המתאים.
OpenKeychain
התחל מחדש
@@ -273,4 +273,104 @@
מקוון
ההודעה הועתקה
הראה מיקום
-
\ No newline at end of file
+ נהל חשבון
+ פרטי צ\'אט קבוצתי
+ פרטי הערוץ
+ %1$s קרס
+ התנתק
+ הפרת מדיניות
+ פסק זמן התחברות
+
+ - %d צ\'אט שלא נקרא
+ - %d צ\'אטים שלא נקראו
+ - %d צ\'אטים שלא נקראו
+
+ שלח דיווחי קריסות
+ האם אתה רוצה למחוק את כל ההודעות בצ\'אט הזה?\n\nאזהרה: זה לא ישפיע על הודעות המאוחסנות במכשירים או שרתים אחרים.
+ תחום לא חוקי
+ שגיאת זרימה
+ האם ברצונך להסיר את הסימניה עבור %s?
+ האם תרצה להסיר את הסימניה עבור %s ולהעביר את הצ\'אט לארכיון?
+ הזמן איש קשר
+ הזמן
+ השימוש בחשבון XMPP שלך לשליחת באגים עוזר לפיתוח מתמשך של %1$s.
+ שתף עם…
+ ארכיון צ\'אט
+ הוסף לפנקס הכתובות
+ חסום משתתף
+ בטל חסימת משתמש
+ בחר אנשי קשר
+ שתף באמצעות חשבון
+ צא\'ט חדש
+ כינוי לא חוקי
+ "האם תרצה להסיר את %s מרשימת אנשי הקשר שלך? הצ\'אט עם איש הקשר הזה לא יוסר."
+ בחר איש קשר
+ חסום
+ לא ניתן להתחבר לחשבון
+ לא ניתן להתחבר למספר חשבונות
+ הקש כדי לנהל את החשבונות שלך
+ האם להוסיף את איש הקשר החסר הזה לרשימת אנשי הקשר שלך?
+ מתכונן לשלוח תמונה
+ מתכון לשלוח תמונות
+ משתף קבצים. נא להמתין…
+ מחק קובץ
+ מחק את הצ\'אט לאחר מכן
+ בחר מכשיר
+ שלח הודעה
+ שלח הודעה ל%s
+ שלח הודעה מוצפנת v\\OMEMO
+ כינוי חדש בשימוש
+ לא ניתן היה להצפין את ההודעה שלך כי אנשי הקשר שלך לא מכריזים על המפתחות הציבוריים שלהם.\n\nבקש מהם להגדיר את OpenPGP.
+ שלח הודעה מוצפנת
+ לא ניתן היה להצפין את ההודעה שלך כי איש הקשר שלך לא הכריז על המפתח הציבורי שלו.\n\nאנא בקש מאיש הקשר שלך להגדיר את OpenPGP.
+ מצורפים
+ התראות
+ רטט כשמגיעה הודעה חדשה
+ התראות שמע
+ התראת LED
+ נורית התראה מהבהבת כשמגיעה הודעה חדשה
+ רינגטון
+ מתקדם
+ על ידי שליחת דיווחי קריסות אתה עוזר לפיתוח
+ אפשר לאנשי הקשר שלך לדעת כאשר קיבלת וקראת את ההודעות שלהם
+ מניעת צילום מסך
+ לא ניתן להמיר את התמונה
+ האפליקציה שבה השתמשת כדי לשתף את הקובץ הזה לא סיפקה מספיק הרשאות.
+ צליל התראה עבור הודעות חדשות
+ OpenKeychain יצר שגיאה.
+ הסתר את תוכן האפליקציה במחליף האפליקציות וחסום צילומי מסך
+ OpenKeychain כדי להצפין ולפענח הודעות ולנהל את המפתחות הציבוריים שלך.
הוא מורשה תחת GPLv3+ וזמין ב-F-Droid וב-Google Play.
(אנא הפעל מחדש את %1$s לאחר מכן.)]]>
+ האם אתה בטוח שברצונך למחוק את הקובץ הזה?\n\nאזהרה: פעולה זו לא תמחק עותקים של קובץ זה המאוחסנים במכשירים או שרתים אחרים.
+ רינגטון לשיחות נכנסות
+ תקופת החסד
+ הרישום אינו נתמך על ידי השרת
+ לקוח לא תואם
+ אסימון רישום לא חוקי
+ משא ומתן TLS נכשל
+ שגיאת פתיחת זרם
+ משך זמן השתקת התראות לאחר זיהוי פעילות באחד מהמכשירים האחרים שלך.
+ UI
+ מפתח הצפנה שגוי.
+ שגיאה
+ האפליקציה שבה השתמשת כדי לבחור תמונה זו לא סיפקה מספיק הרשאות לקרוא את הקובץ.\n\nהשתמש במנהל קבצים אחר כדי לבחור תמונה.
+ עטיפת ערוץ אינה זמינה
+ הסר את המפתח הציבורי של OpenPGP
+ האם אתה בטוח שברצונך להסיר את מפתח OpenPGP הציבורי שלך מהודעת הנוכחות שלך?\nאנשי הקשר שלך לא יוכלו יותר לשלוח לך הודעות מוצפנות OpenPGP.
+ מפתח ציבורי OpenPGP פורסם.
+ האם אתה בטוח שברצונך למחוק את חשבונך? מחיקת החשבון שלך מוחקת את כל היסטוריית הצ\'אט שלך
+ כתובת XMPP
+ האם ברצונך להוסיף את %s לפנקס הכתובות שלך?
+ חסום כתובת XMPP
+ זו אינה כתובת XMPP חוקית
+ נגמר הזיכרון. תמונה גדולה מדי
+ XEP-0215: גילוי שירות חיצוני
+ XEP-0357: דחיפה
+ נראה לאחרונה לפני דקה
+ נראה לאחרונה לפני שעה
+ נראה לאחרונה לפני יום אחד
+ הודעה מוצפנת. אנא התקן את OpenKeychain כדי לפענח אותו.
+ נמצאו הודעות מוצפנות חדשות של OpenPGP
+ מזהה מפתח OpenPGP
+ XEP-0386: כריכה 2
+ XEP-0388: פרופיל SASL הניתן להרחבה
+
diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml
index 5dec1fa5f0fd6a979a557bd0e6ba2343f4e184be..f2ebaa7f9bb097faa2d49880f76bfd346db77385 100644
--- a/src/main/res/values-ja/strings.xml
+++ b/src/main/res/values-ja/strings.xml
@@ -242,7 +242,7 @@
グループチャットに参加しています…
退出
連絡先があなたを連絡先リストに追加しました
- 戻りを追加
+ 連絡先を追加
%s はここまで読みました
%s はここまで読みました
%1$s +%2$d人がここまで読みました
@@ -317,7 +317,7 @@
フォアグラウンドサービス
OSが接続を切断するのを防止します
バックアップを作成
- バックアップファイルは %s に保存されます
+ バックアップファイルは %s に保存されます
バックアップファイルを作成しています
バックアップを作成しました
バックアップファイルは %s に保存されました
@@ -1090,4 +1090,8 @@
背景色、文字サイズ、プロフィール画像など
ふきだし
接続タイムアウト
+ OMEMO鍵を復元
+ Quicksyはquicksy.imのアカウントのバックアップしか復元できません
+ バックアップの保存先
+ Word 文書
diff --git a/src/main/res/values-kab/strings.xml b/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a89a0bfb67f191c98ea9f6cf621ce5c5e4427f01
--- /dev/null
+++ b/src/main/res/values-kab/strings.xml
@@ -0,0 +1,234 @@
+
+
+ xmpp.example.com
+ Ddu
+ Immed
+ Snulfu-d
+ OpenKeychain
+ Yal tikkelt
+ Meferisem
+ Asnas
+ Asefrek n imiḍan
+ URL n ufaylu
+ Aleqqem…
+ Ssens-it
+ Uqqin
+ Ur yestuffi ara
+ Iɣewwaṛen
+ Sefrek amiḍan
+ Sefrek imiḍan
+ Talqayt unermis
+ Rnu amiḍan
+ Snifel isem
+ Talqayt n wegraw n usqerdec
+ Bḍu akked …
+ Fren anermis
+ Fren inermisen
+ Asqerdec amaynut
+ tura yakan
+ Bḍu-t s umiḍan
+ Tuzzna…
+ Anebdal
+ Bab-is
+ Imḍebbar
+ Imttekki
+ Anermis
+ Semmet
+ Rnu
+ Ẓreg
+ Kkes
+ Sewḥel
+ Sekles
+ Ih
+ Bḍu akked …
+ Snubeg anermis
+ Azen-it tura
+ Seddu afaylu
+ Rnu anermis
+ Sfeḍ amazray
+ Kkes afaylu
+ Fren ibenk
+ Azen izen
+ Ales asenker
+ Sebded
+ Ṛǧu…
+ Amatu
+ Aṭenṭen
+ Iɣewwaren leqqayen
+ Qbel
+ Tuccḍa
+ Amiḍan-inek·inem
+ Fren tugna
+ Ṭṭef tawlaft
+ D arussin
+ Uqqin
+ Tuqqna…
+ Aruqqin
+ Ur yettusireg ara
+ Teffɣeḍ
+ Ulac tuqqna
+ OTR
+ OpenPGP
+ OMEMO
+ Kkes amiḍan
+ aseqdac@example.com
+ Awal n uɛeddi
+ Sermed amiḍan-a
+ Tansa XMPP
+ XEP-0313: MAM
+ Ibenkan niḍen
+ Kkes anermis
+ Sewḥel anermis
+ Fren
+ Ttekki
+ Asentel
+ Suffeɣ
+ Uḍfir
+ Zgel
+ Sermed
+ i %s
+ Zgel
+ Taɣellist
+ Wiyyaḍ
+ Sekcem awal n uɛeddi
+ Suter tura
+ Iɣewwaṛen leqqayen
+ Ɣef %s
+ Sentem
+ tansa web
+ Talqayt n umiḍan
+ Ɛreḍ tikkelt nniḍen
+ afaylu
+ Fren afaylu
+ Zḍem-d %S
+ Kkes %s
+ Ldi %s
+ Afaylu yettwakkes
+ Kkes ibenkan
+ Awal uffir yettusnifel!
+ Snifel awal n uɛeddi
+ Awal n uɛeddi amiran
+ Aruqqin
+ Aεeggal
+ Werǧin
+ Askar alqayan
+ Err
+ ameslaw
+ tavidyutt
+ tugna
+ isemli PDF
+ Anermis
+ Isemli Word
+ Asnas Android
+ Tuzzna n %s
+ Ula d yiwen
+ Tigawt taruradt
+ Isem n useqdac
+ Isem n useqdac
+ Nadi inermisen
+ Tuqqna
+ Asenneftaɣ
+ Tawwurt
+ Seɣti izen
+ Ulac-it
+ Snulfu-d amiḍan
+ Ssens-it
+ Tabaḍnit
+ Asentel
+ Awurman
+ Aceεlal
+ Ubrik
+ Taṭablit
+ Tadiwent
+ Nekk
+ Sireg
+ Iminig web
+ Izen n tuccḍa
+ Ass-a
+ Iḍelli
+ Izen
+ Ldi asmel web
+ Sekles tavidyutt
+ Tikkelt
+ Arewway:
+ Bḍu
+ GIF
+ Isem
+ Rnu anegzum
+ Nɣel adeg
+ Bḍu adeg
+ Bḍu adeg
+ Sken-d adeg
+ Ttxil rǧu…
+ Nadi deg iznan
+ Isem n unermis
+ Iznan
+ Iznan
+ Imttekkiyen
+ Yettwasefsex
+ Taɣara n tvidyutt
+ D talemmast (360p)
+ Uɣal
+ Ih
+ Uhu
+ Uṭṭun n tiliɣri
+ Nadi timura
+ Asenqed iteddu…
+ Isem-ik·im
+ Sekker Orbot
+ Ldi s…
+ Fren amiḍan
+ Tansa XMPP
+ Nadi imttekkiyen
+ Tadyant
+ jabber.network
+ Aqeddac adigan
+ Yettṣuni…
+ Ur yestuffi ara
+ Tallalt
+ Asiwel iteddu
+ Asiwel i ikcem-d
+ Asiwel s uvidyu
+ Ffeɣ
+ Asqerdec-a
+ Amiḍan XMPP
+ Agi
+ Aqeddac Push
+ Senser
+ Qqen
+ Bdu asqerdec
+ Awgelhen seg yixef ɣer wayeḍ
+ Anagraw n wammud
+ Tamyigawt
+ Ɣef yibenk
+ Inermisen
+ Nadi
+ Aselkim
+ Yetteqqen
+ D uqqin
+ Agrudem
+ Qqen
+ semmet
+ Aleqqem
+ e-book
+ Seddu
+ Err-it-id
+ Ɣef
+ Agi
+ Udem
+ azen-it tikkelt-nniḍen
+ Sebded Orbot
+ Siwel
+ Asiwel i ikcem-d
+ Ugar n yiɣewwaṛen
+ Tasertit n tbaḍnit
+ Azdam awurman
+ Tuqqna ɣer uqeddac
+ Taɣellist
+ Tuzna
+ Anasiw
+ Rnu anermis
+ Awal n uɛeddi amaynut
+ Wali asqerdec
+ Rnu-t akken yebɣu yili
+
diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml
index b1fda75ca032cf5693812bb800b8e4347f27b70e..6b97d7e8ec9d15222d27b3349d079382e56a5715 100644
--- a/src/main/res/values-nl/strings.xml
+++ b/src/main/res/values-nl/strings.xml
@@ -1127,4 +1127,4 @@
Time-out voor verbinding
Opnieuw proberen met P2P
Kanaalbinding onbeschikbaar
-
\ No newline at end of file
+
diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml
index cee734c977af0b2f8d630d1b18fa1b2e5294d78f..29de9e4f3b987e66639fac36392aec0ce0811b4e 100644
--- a/src/main/res/values-pl/strings.xml
+++ b/src/main/res/values-pl/strings.xml
@@ -325,7 +325,7 @@
Usługa na pierwszym planie
Uniemożliwia systemowi przerwanie połączenia
Utwórz kopię zapasową
- Kopia zapasowa będzie zapisana w %s
+ Kopie zapasowe będą przechowywane w %s
Tworzenie kopii zapasowej
Kopia zapasowa została utworzona
Kopia zapasowa zapisana w %s
@@ -415,7 +415,7 @@
obraz
grafika wektorowa
plik multimediów
- Dokument PDF
+ dokument PDF
Aplikacja Androida
Kontakt
Avatar został pomyślnie opublikowany!
@@ -464,7 +464,7 @@
Pobieranie niepowiodło się: brak możliwości zapisu pliku
Pobieranie nieudane: Nieprawidłowy plik
Sieć TOR jest niedostepna
- Błąd połączenia (zasób)
+ Błąd przywiązania kanału (zasobu)
Nie odpowiada za domenę
Zepsute
Dostępność
@@ -841,12 +841,12 @@
e-book
Oryginalne (nieskompresowane)
Otwórz za pomocą…
- Obrazek profilowy Conversations
+ Awatar
Wybierz konto
Przywróć kopię zapasową
Przywróć
Wpisz swoje hasło do konta %s aby przywrócić kopię zapasową.
- Nie używaj kopii zapasowej aby klonować (uruchamiać równolegle) instalację. Przywracanie kopii jest przeznaczone tylko do migracji albo kiedy urządzenie zostało zgubione.
+ Nie przywracaj kluczy OMEMO aby klonować (uruchamiać równocześnie) instalację. Przywracanie kluczy OMEMO jest przeznaczone tylko do migracji albo kiedy urządzenie zostało zgubione.
Nie można przywrócić kopii zapasowej.
Nie można odszyfrować kopii zapasowej. Czy hasło jest poprawne?
Kopia i Przywracanie
@@ -1019,7 +1019,7 @@
Nie można usunąć konta z serwera
Przeszukaj rozmowy grupowe
Rozmowy grupowe
- Nie próbuj przywracać kopii zapasowych, których nie utworzono samodzielnie!
+ Przywracaj jedynie kopie zapasowe, które samodzielnie utworzyłeś.
Próbujesz zaimportować plik kopii zapasowej o przestarzałym formacie
Audiobook
Połącz się ponownie na innym hoście
@@ -1140,4 +1140,17 @@
Pokazuj wyłącznie kontaktom
Limit czasu połączenia
Spróbuj ponownie używając P2P
+ dokument Microsoft Word
+ Przywiązywanie kanału niedostępne
+ Przywróć klucze OMEMO
+ Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im
+ Lokalizacja kopii zapasowej
+ URI
+ Kopiuj numer telefonu
+ Kopiuj lokalizację geograficzną
+ Kopiuj adres e‑mail
+ Skopiowano numer telefonu do schowka
+ Kopiuj URI
+ Skopiowano adres e‑mail do schowka
+ Skopiowano URI do schowka
diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml
index cc0d8ac559709f82b7310c67470bf2e764ee1459..1d4c635c8e558576497e5404f84f570fea2e95cb 100644
--- a/src/main/res/values-pt-rBR/strings.xml
+++ b/src/main/res/values-pt-rBR/strings.xml
@@ -281,7 +281,7 @@
Aviso: Enviar isso sem atualizações mútuas de presença pode provocar problemas inesperados.\n\nVerifique nos detalhes do contato suas inscrições de presença.
Segurança
Correção de mensagem
- Permita que seus contatos editem suas mensagens retroativamente.
+ Permita que seus contatos editem suas mensagens retroativamente
Configurações avançadas
Por favor, tenha cuidado com isso
Sobre o %s
@@ -326,7 +326,7 @@
Serviço ativo
Impede que o sistema operacional encerre sua conexão
Criar backup
- Os arquivos de backup serão armazenados em %s
+ Os backups serão armazenados em %s
Criando arquivos de backup
O seu backup foi criado
Os arquivos de backup foram armazenados em %s
@@ -832,12 +832,12 @@
e-book
Original (não comprimido)
Abrir com…
- Imagem de perfil do Conversations
+ Avatar
Selecione a conta
Restaurar o backup
Restaurar
Digite sua senha para a conta %s para restaurar o backup.
- Não use o recurso de restaurar um backup para tentar clonar (rodar simultaneamente) uma instalação. A restauração de backups é destinada a migrações ou caso você tenha perdido o dispositivo original.
+ Não restaure chaves OMEMO na tentativa de clonar (rodar simultaneamente) uma instalação. A restauração de chaves OMEMO é destinada a migrações ou caso você tenha perdido o dispositivo original.
Não foi possível restaurar o backup.
Não foi possível descriptografar o backup. A senha está correta?
Backup & Restauração
@@ -1042,7 +1042,7 @@
Esconder notificação
Ao atuar como um distribuidor UnifiedPush, a conexão persistente, estável, e amigável à bateria do XMPP será usada para alertar outros apps compatíveis com o UnifiedPush, como o Tusky, Ltt.RS, FluffyChat, e mais.
Conversas correspondentes arquivadas.
- Não tente restaurar backups que você não criou!
+ Restaure somente backups que você mesmo criou.
O contato não está disponível
O vídeo está ativado. Toque para desativar.
A descoberta de canais usa um serviço de terceiros chamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Usar esta funcionalidade transmitirá seu endereço de IP e termos de pesquisa ao serviço. Leia sua <a href=https://search.jabber.network/privacy>Política de Privacidade</a> para mais informações.
@@ -1129,4 +1129,16 @@
Conexão demorou muito
Tentar novamente com P2P
Vínculo de canal indisponível
+ Documento do Word
+ Restaurar as chaves OMEMO
+ O Quicksy só pode restaurar backups de contas quicksy.im
+ Local do backup
+ URI
+ Copiar URI
+ Copiar número de telefone
+ Copiar localização geográfica
+ Copiar endereço de e-mail
+ Endereço de e-mail copiado para a área de transferência
+ URI copiada para a área de transferência
+ Número de telefone copiado para a área de transferência
diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml
index 4ecd5d6ca4a4513b656ba6c3b618251f23dc2b73..2ab8185486fe58403be5671da91246f09c921fec 100644
--- a/src/main/res/values-ro-rRO/strings.xml
+++ b/src/main/res/values-ro-rRO/strings.xml
@@ -173,7 +173,7 @@
OMEMO
Șterge cont
Dezactivare temporară
- Publică avatar
+ Publică poză profil
Publică cheia publică OpenPGP
Șterge cheia publică OpenPGP
Sigur doriți să vă ștergeți cheia publică OpenPGP din mesajele de prezență?\nContactele dumneavoastră nu vor mai putea să vă trimită mesaje criptate cu OpenPGP.
@@ -196,7 +196,7 @@
XEP-0237: Creare de versiuni listă
XEP-0198: Management flux
XEP-0215: Descoperirea serviciilor externe
- XEP-0163: PEP (Avatare / OMEMO)
+ XEP-0163: PEP (Poză profil / OMEMO)
XEP-0363: Încărcare fișiere prin HTTP
XEP-0357: Push
disponibil
@@ -252,13 +252,13 @@
%1$s și încă %2$d au citit până aici
Toate persoanele au citit până aici
Publică
- Atingeți avatarul pentru a selecta o poză din galerie
+ Atingeți poza de profil pentru a selecta o poză din galerie
Se publică…
Acest server v-a refuzat publicarea
Nu s-a putut face convertirea pozei
- Nu s-a putut salva avatarul pe disc
+ Nu s-a putut salva poza de profil pe disc
(Sau apasă îndelung pentru a reseta la implicit)
- Serverul dumneavoastră nu permite publicarea de avatare
+ Serverul dumneavoastră nu permite publicarea de poze de profil
șoptește
către %s
Trimite mesaj privat catre %s
@@ -324,7 +324,7 @@
Serviciul activ în prim-plan
Previne închiderea conexiunii de către sistemul de operare
Creează o copie de siguranță
- Fișierele copiei de siguranță vor fi salvate în %s
+ Copiile de siguranță vor fi salvate în %s
Se creează copia de siguranță
Copia de siguranță a fost creată
Fișierele copiei de siguranță au fost salvate în %s
@@ -352,7 +352,7 @@
Activează notificările
Nu s-a găsit serverul pentru discuția de grup
Nu s-a putut crea discuția de grup
- Avatar cont
+ Poză de profil cont
Copiază amprenta OMEMO în memorie
Generează din nou cheia OMEMO
Curață lista dispozitivelor
@@ -417,7 +417,7 @@
document PDF
Aplicație Android
Contact
- Avatarul a fost publicat!
+ Poza de profil a fost publicată!
Trimit %s
Ofer %s
Ascunde deconectat
@@ -593,7 +593,7 @@
Șterge identitățile OEMO
Regenerează cheile personale OMEMO. Toate contactele vor fi obligate să verifice cheile dumneavoastră din nou. Folosiți asta doar ca o ultimă opțiune.
Șterge cheile selectate
- Pentru a putea publica avatarul trebuie să existe o conexiune.
+ Pentru a putea publica poza de profil trebuie să existe o conexiune.
Arată mesaj de eroare
Mesaj de eroare
Economizorul de date este activat
@@ -741,9 +741,9 @@
Partajare fișiere prin HTTP pentru S3
Activează direct căutarea
În ecranul \"Discuție nouă\" focalizează câmpul de căutare și arată tastatura
- Avatar discuție de grup
- Serverul gazdă nu suporta avatare pentru grupuri
- Doar proprietarul grupului poate schimba avatarul
+ Poză de profil discuție de grup
+ Serverul gazdă nu suporta poze de profil pentru grupuri
+ Doar proprietarul grupului poate schimba poza de profil
Nume contact
Numele dumneavoastră
Titlu discuție de grup
@@ -833,12 +833,12 @@
carte electronică
Original (necompresat)
Deschide cu…
- Poză profil Conversations
+ Poză de profil
Alegeți contul
Restaurează o copie de siguranță
Restaurează
Introduceți parola contului %s pentru a restaura copia de siguranță.
- Nu folosiți funcția de restaurare a copiei de siguranță pentru a încerca clonarea (rularea simultană a) instalării. Restaurarea copiei de siguranță este gândită doar pentru a migra pe un alt dispozitiv sau în cazul în care ați pierdut dispozitivul original.
+ Nu restaurați cheile OMEMO în încercarea de a clona (rula simultan) aplicația. Restaurarea cheilor OMEMO este destinată doar migrărilor sau în cazul în care ați pierdut dispozitivul original.
Nu s-a putut restaura copia de siguranță.
Nu s-a putut decripta copia de siguranță. Este parola corectă?
Copie de siguranță & Restaurare
@@ -956,9 +956,9 @@
Nu s-a putut corecta mesajul
Toate discuțiile
Această discuție
- Avatarul dumneavoastră
- Avatar pentru %s
- Criptare OMEMO
+ Poza dumneavoastră de profil
+ Poză de profil pentru %s
+ Criptat cu OMEMO
Criptare OpenPGP
Fără criptare
Ieșire
@@ -989,7 +989,7 @@
Nu este posibilă înregistrarea unui cont
Nu a fost găsită o adresă XMPP
Eroare temporară de autentificare
- Șterge avatar
+ Șterge poza de profil
Apelurile sunt dezactivate atunci când utilizați Tor
Comută la video
Respinge solicitarea de comutare la video
@@ -1006,7 +1006,7 @@
Șterge contul de pe server
Discuții de grup
Caută discuții de grup
- Nu încercați să restaurați copii de rezervă pe care nu le-ați creat personal!
+ Restaurați numai copii de rezervă pe care le-ați creat personal.
Încercați să importați un fișier copie de rezervă format vechi
Carte audio
Reconectat pe altă gazdă
@@ -1088,7 +1088,7 @@
Atunci când dispozitivul este blocat permite aplicației să arate notificările apelurilor pe tot ecranul.
Permite mesaje private
Nu s-a putut dezactiva videoul.
- Avatarul dumneavoastră. Atingeți pentru a selecta un nou avatar din galerie.
+ Poza dumneavoastră de profil. Atingeți pentru a selecta una nouă din galerie.
Schimbă setările notificărilor
Schimbă configurația
Editare nume și subiect
@@ -1112,20 +1112,32 @@
Mai multe reacții
Nu s-a putut modifica apelul
Clientul XMPP al contactului dvs. este posibil să nu accepte apeluri audio/video.
- Culoare de fundal, mărime font, avatare
- Afișați avatare pentru mesajele dvs. și în discuțiile 1:1, în plus față de cele de grup.
+ Culoare de fundal, mărime font, poze profil
+ Afișați poze de profil pentru mesajele dvs. și în discuțiile 1:1, în plus față de cele de grup.
Bule de mesaj
Bule de mesaj
- Arată avatare
+ Arată poze de profil
Activați setările de notificare personalizate (importanță, sunet, vibrații) pentru această conversație?
Notificări personalizate
Afișați toate mesajele, inclusiv cele trimise, în partea stângă pentru un aspect uniform al discuției.
Mesaje aliniate la stânga
Apelurile din această aplicație interacționează cu apelurile telefonice obișnuite, cum ar fi terminarea unui apel atunci când începe altul.
Integrarea apelurilor
- Ați dori să vă ștergeți avatarul? Unii clienți ar putea să continue să arate o copie a avatarului.
+ Ați dori să vă ștergeți poza de profil? Unii clienți ar putea să continue să arate o copie a ei.
Arată doar contactelor
Timp limită de conectare expirat
Reîncearcă cu P2P
Channel binding indisponibil
+ Document Word
+ Quicksy poate restaura doar copiile de rezervă pentru conturile quicksy.im
+ Restaurare chei OMEMO
+ Locație copie de siguranță
+ adresă
+ Copiere adresă
+ Copiere locație
+ Telefon copiat
+ Copiere e-mail
+ Adresă copiată
+ Copiere telefon
+ E-mail copiat
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml
index 18cc178a3ce0d7ec6749f362ace24dd2f91d670d..972c2aaa94d3e184216cce469533db1c05b5b4d4 100644
--- a/src/main/res/values-ru/strings.xml
+++ b/src/main/res/values-ru/strings.xml
@@ -91,7 +91,7 @@
Отправить сообщение без шифрования
Сообщение
Сообщение для %s
- Зашифрованное vOMEMO сообщение
+ Зашифрованное v\\OMEMO сообщение
Используется новое имя
Отправить в незашифрованном виде
Расшифровка невозможна. Вероятно, у вас нет надлежащего ключа.
@@ -218,9 +218,9 @@
Найдены новые зашифрованные OpenPGP сообщения
ID ключа OpenPGP
Отпечаток OMEMO
- Отпечаток vOMEMO
+ Отпечаток v\\OMEMO
Отпечаток OMEMO (выбранного сообщения)
- Отпечаток vOMEMO (выбранного сообщения)
+ Отпечаток v\\OMEMO (выбранного сообщения)
Другие устройства
Доверенные отпечатки OMEMO
Получение ключей…
@@ -332,7 +332,7 @@
Процесс переднего плана
Не позволять операционной системе закрывать ваше соединение
Создать резервную копию
- Файлы резервной копии будут сохранены в %s
+ Резервные копии будут сохранены в %s
Создание резервной копии
Ваша резервная копия создана
Файлы резервной копии сохранены в %s
@@ -422,7 +422,7 @@
видео
изображение
векторная графика
- PDF-документ
+ Документ PDF
Приложение Android
Контакт
Аватар загружен!
@@ -847,12 +847,12 @@
Электронная книга
Оригинал (без сжатия)
Открыть с помощью…
- Изображение профиля Conversations
+ Аватар
Выбрать аккаунт
Восстановить из резервной копии
Восстановить
Введите пароль аккаунта %s для восстановления резервной копии.
- Не используйте восстановление резервной копии для дублирования установленного приложения (одновременного исполнения). Восстановление резервной копии нужно лишь для того, чтобы перенести данные на другое устройство или на случай потери своего устройства.
+ Не используйте восстановление ключей OMEMO для дублирования установленного приложения (одновременного использования). Восстановление ключей OMEMO нужно только для переноса данных на другое устройство или на случай потери своего устройства.
Невозможно восстановить резервную копию.
Невозможно расшифровать резервную копию. Вы ввели верный пароль?
Резервное копирование и восстановление
@@ -1022,7 +1022,7 @@
Аккаунт для получения push-уведомлений
Нет (неактивно)
Вы собираетесь проверить ключи OMEMO своего аккаунта. Это безопасно только в том случае, если вы перешли по этой ссылке из надёжного источника, где только вы могли опубликовать эту ссылку.
- Не пытайтесь восстановить резервные копии, которые не были созданы вами!
+ Восстанавливайте только те резервные копии, которые были созданы лично вами!
- %1$d пропущенный вызов от %2$s
- %1$d пропущенных вызова от %2$s
@@ -1158,4 +1158,16 @@
Истекло время ожидания подключения
Повторить через P2P
Привязка канала недоступна
+ Документ Word
+ Восстановить ключи OMEMO
+ Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im
+ Расположение резервной копии
+ URI скопирован в буфер обмена
+ URI
+ Копировать номер телефона
+ Номер телефона скопирован в буфер обмена
+ Копировать местоположение
+ Копировать адрес почты
+ Копировать URI
+ Адрес электронной почты скопирован в буфер обмена
diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml
index 750a55ebbeea052f2a100600ad814c501e3b9728..bb53c00db912f6e51b3b9038258cff0b1385a5e2 100644
--- a/src/main/res/values-sk/strings.xml
+++ b/src/main/res/values-sk/strings.xml
@@ -500,4 +500,4 @@
Zašifrované s OMEMO
Zlyhané doručenia
Viac možnosťí
-
\ No newline at end of file
+
diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml
index b8c64aba80be703b19ccb5b5d8ca0941584fbd73..aa61160ab1df609586ac5276341f3a509c48e7f0 100644
--- a/src/main/res/values-sq-rAL/strings.xml
+++ b/src/main/res/values-sq-rAL/strings.xml
@@ -253,7 +253,7 @@
Riprovoni
Shërbim në prapaskenë
Krijo kopjeruajtje
- Kartelat kopjeruajtje do të depozitohen në %s
+ Kopjeruajtjet do të depozitohen në %s
Po krijohen kartela kopjeruajtje
Kopjeruajtja juaj u krijua
Po rikthehet kopjeruajtje
@@ -623,7 +623,7 @@
e-libër
Origjinalja (e pangjeshur)
Hape me…
- Foto profili Conversations
+ Avatar
Zgjidhni llogari
Riktheje kopjeruajtjen
Riktheje
@@ -953,7 +953,7 @@
%s
Dakord, apo do të donit të përpunonit numrin??]]>
%s.]]>
Që të rikthehet kopjeruajtja, jepni fjalëkalimin tuaj për llogarinë %s.
- Mos përdorni veçorinë e rikthimit të një kopjeruajtje në një përpjekje për të klonuar (xhiruar në të njëjtën kohë) një instalim. Rikthimi i një kopjeruajtje është menduar vetëm për migrime, ose në rast se humbët pajisjen origjinale.
+ Mos riktheni kyçe OMEMO, në një përpjekje për të klonuar (xhiruar në të njëjtën kohë) një instalim. Rikthimi i kyçeve OMEMO është menduar vetëm për migrime, ose në rast se humbët pajisjen origjinale.
Ky kanal publik s’ka pjesëmarrës. Ftoni kontaktet tuaj, ose përdorni butonin e ndarjes me të tjerët për të dhënë adresën XMPP të tij.
Aplikacioni dhënës nuk akordoi leje për hyrje në këtë kartelë.
Shumica e përdoruesve duhet të zgjedhin ‘jabber.network’ për sugjerime më të mira nga krejt ekosistemi publik XMPP.
@@ -968,7 +968,7 @@
- %1$d thirrje të humbur prej %2$d kontakti
- %1$d thirrje të humbur prej %2$d kontaktesh
- Mos u rrekni të riktheni kopjeruajtje që s’i keni krijuar ju vetë!
+ Riktheni vetëm kopjeruajtje që ’i keni krijuar ju vetë.
Të shtohen pjesë shtesë?
XEP-0280: Message Carbons
Rilidhu te një tjetër strehë
@@ -1121,4 +1121,16 @@
Doni të fshihet avatari jua? Disa klientë mund të vazhdojnë të shfaqin një kopje të ruajtur në fshehtinat e tyre të avatarit tuaj.
Mbarim kohe për lidhjen
Riprovo me P2P
-
\ No newline at end of file
+ Dokument Word
+ Rikthe kyçe OMEMO
+ Quicksy mund të rikthejë vetëm kopjeruajtje për llogari quicksy.im
+ Vendndodhje kopjeruajtjesh
+ URI
+ Kopjo numër telefoni
+ Kopjo vendndodhje gjeografike
+ Kopjo adresë email
+ Numri i telefonit u kopjua në të papastër
+ URI u kopjua në të papastër
+ Adresa email u kopjua në të papastër
+ Kopjo URI-n
+
diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml
index ea16f960a41d82307e52fb6a271c1b94f0deb929..969f9a3372c221f59c2b3ef1fa8cbf3714f73ac6 100644
--- a/src/main/res/values-sr/strings.xml
+++ b/src/main/res/values-sr/strings.xml
@@ -314,7 +314,7 @@
Копирај оригинални линк
Пошаљи поново
Линк ка фајлу
- Линк је копиран у клипборд
+ Линк копиран у клипборд
XMPP адреса копирана у клипборд
Текст грешке копиран у клипборд
веб адреса
@@ -720,9 +720,9 @@
Прикажи садржај
Учесници
Прегледач садржаја
- Conversations профилна слика
+ Аватар
Отвори користећи…
- Не покушавај да вратиш резервну копију коју ниси направио/ла сам!
+ Враћај само оне резервне копије које си сам/а направио/ла.
Направи јавни канал
Дозволи било коме да измени тему
Подели резервне копије
@@ -1005,7 +1005,7 @@
Инсталирај Orbot
Врати
Унеси своју лозинку за налог %s да вратиш резервну копију.
- Не употребљавај функцију враћања резервне копије ради клонирања инсталације (за рад у паралели). Враћање резервне копије је предвиђено само за миграције или у случају да си изгубио/ла оригинални уређај.
+ Не враћај резервну копију OMEMO кључева ради клонирања инсталације (за рад у паралели). Враћање резервне копије OMEMO кључева је предвиђено само за миграције или у случају да си изгубио/ла оригинални уређај.
Није могуће вратити резервну копију.
Није могуће дешифровати резервну копију. Да ли је лозинка исправна?
Резервна копија и Враћање
@@ -1145,4 +1145,16 @@
Истекла веза
Покушај поново са P2P
Везивање канала недоступно
+ Word документ
+ Quicksy може да врати резервне копије само за quicksy.im налоге
+ Локација резервних копија
+ Врати OMEMO кључеве
+ URI копиран у клипборд
+ URI
+ Копирај геолокацију
+ Копирај имејл адресу
+ Копирана имејл адреса у клипборд
+ Копирај број телефона
+ Копиран број телефона у клипборд
+ Копирај URI
diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml
index 8bb5d851c860773aae06d2400f363de648e7fa42..90786334e60d26033d9f645d930614c642eb64c4 100644
--- a/src/main/res/values-szl/strings.xml
+++ b/src/main/res/values-szl/strings.xml
@@ -981,4 +981,4 @@
Dokumynt ze samym tekstym
Registracyjo kōnt niy je spiyrano
Żodno adresa XMPP niyznojdziōno
-
\ No newline at end of file
+
diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml
index fa6b360de767edcdaf6157072f69c313d6941f37..9c1affde47951c11367cbbee75ac85c8ab552df9 100644
--- a/src/main/res/values-tr-rTR/strings.xml
+++ b/src/main/res/values-tr-rTR/strings.xml
@@ -1068,4 +1068,4 @@
Yabancılardan gelen davetler
Yabancılardan gelen grup davetlerini kabul et
Yapılandırmayı değiştir
-
\ No newline at end of file
+
diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml
index 5d3ff193f1790c1e7e353028c576f465a4465de2..efe90a297c0550d4f253f646cfa7dd2cd9a52708 100644
--- a/src/main/res/values-uk/strings.xml
+++ b/src/main/res/values-uk/strings.xml
@@ -305,7 +305,7 @@
URL файлу
URL скопійовано
Адресу XMPP скопійовано
- Текст повідомлення про помилку скопійовано
+ Повідомлення про помилку скопійовано
вебадреса
Розпізнати QR-код
Показати QR-код
@@ -701,7 +701,7 @@
Доступ до місцезнаходження вимкнено
Закріпити розташування
Відкріпити розташування
- Скопіювати місцезнаходження
+ Копіювати місцезнаходження
Поділитися місцезнаходженням
Напрямки
Поділитися місцезнаходженням
@@ -808,12 +808,12 @@
Електронна книга
Оригінал (нестиснений)
Відкрити…
- Зображення профілю для Conversations
+ Аватар
Виберіть обліковий запис
Відновити з резервної копії
Відновити
Введіть пароль до облікового запису %s, щоб відновити з резервної копії.
- Не використовуйте відновлення з резервної копії з метою клонування застосунку (запускати одночасно ще один примірник). Відновлення з резервної копії призначене виключно для перенесення даних або на випадок втрати оригінального пристрою.
+ Не відновлюйте ключі OMEMO з метою клонування застосунку (запускати одночасно ще один примірник). Відновлення ключів OMEMO призначене виключно для перенесення даних або на випадок втрати оригінального пристрою.
Неможливо відновити з резервної копії.
Не вдалося розшифрувати резервну копію. Чи правильний пароль?
Створити або відновити резервну копію
@@ -1035,7 +1035,7 @@
Не знайдено застосунку
Дистриб\'ютор UnifiedPush
Додати ще пісні\?
- Не намагайтеся відновити резервні копії, які створили не Ви!
+ Відновлюйте лише ті резервні копії, які Ви створили особисто.
Ви намагаєтеся імпортувати файл резервної копії у застарілому форматі
аудіокнига
Відновити з\'єднання на іншому вузлі
@@ -1157,4 +1157,16 @@
Час очікування з\'єднання вичерпано
Повторити спробу з P2P
Прив\'язка каналу недоступна
+ документ Word
+ Відновити ключі OMEMO
+ Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im
+ Розташування резервних копій
+ URI
+ Адресу Email скопійовано
+ Копіювати URI
+ Копіювати номер телефону
+ Копіювати місцезнаходження
+ Номер телефону скопійовано
+ Копіювати адресу Email
+ URI скопійовано
diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml
index 1a57e14bb21e9f23e1d80b88f6f405adb123ba3c..6b21c4183a7d54e2741df23c68ed761c7bca270d 100644
--- a/src/main/res/values-vi/strings.xml
+++ b/src/main/res/values-vi/strings.xml
@@ -975,4 +975,4 @@
Từ chối yêu cầu chuyển sang video
Xóa tài khoản khỏi máy chủ
Không thể xóa tài khoản khỏi máy chủ
-
\ No newline at end of file
+
diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml
index 7539b872e449e67e780f9eca46a7989951ca435c..329e13d18e0560d482430e2b9a1dbea2802df585 100644
--- a/src/main/res/values-zh-rCN/strings.xml
+++ b/src/main/res/values-zh-rCN/strings.xml
@@ -75,7 +75,7 @@
正在准备发送图片
正在准备发送图片
正在分享文件。请稍候…
- 清除历史记录
+ 清除历史
清除聊天记录
是否要删除此对话中的所有消息?
\n
@@ -93,7 +93,7 @@
发送未加密
解密失败。也许您没有合适的私钥。
OpenKeychain
- OpenKeychain 来加密和解密消息并管理公钥。
它在 GPLv3+ 许可证下授权并可在 F-Droid 和 Google Play 上获得。
(请之后重启 %1$s。)]]>
+ OpenKeychain 来加密和解密消息并管理公钥。
它采用 GPLv3+ 许可,可在 F-Droid 和 Google Play 上获取。
(之后请重启 %1$s。)]]>
重启
安装
请安装 OpenKeychain
@@ -108,8 +108,8 @@
\n
\n请通知对方设置 OpenPGP。
常规
- 接收文件
- 自动接收小于此大小的文件…
+ 接受文件
+ 自动接受小于此大小的文件…
附件
通知
振动
@@ -182,7 +182,7 @@
\n您的联系人将无法再向您发送 OpenPGP 加密消息。
OpenPGP 公钥已发布。
启用账号
- 是否确定要删除账号?删除账号会清除全部聊天记录
+ 是否确定要删除账号?删除账号会清空全部聊天记录
录制语音
XMPP 地址
屏蔽 XMPP 地址
@@ -248,8 +248,8 @@
\n警告:将在服务器上完全移除频道。
无法解散群聊
无法解散频道
- 编辑群聊话题
- 话题
+ 编辑群聊主题
+ 主题
正在加入群聊…
离开
对方已将您添加到联系人列表
@@ -288,7 +288,7 @@
\n请前往“联系人详情”以验证在线状态订阅。
安全
消息更正
- 允许您的联系人发送后编辑其消息
+ 允许您的联系人重新编辑已发送的消息
专家设置
请谨慎设置
关于 %s
@@ -320,9 +320,9 @@
复制原始 URL
再次发送
文件 URL
- 已复制 URL 到剪贴板
- 已复制 XMPP 地址到剪贴板
- 已复制出错信息到剪贴板
+ URL 已复制到剪贴板
+ XMPP 地址已复制到剪贴板
+ 错误消息已复制到剪贴板
网址
扫描二维码
显示二维码
@@ -333,7 +333,7 @@
前台服务
防止操作系统中断连接
创建备份
- 备份文件将存储在 %s
+ 备份将存储在 %s
正在创建备份文件
备份已创建
此备份文件已存储在 %s
@@ -365,7 +365,7 @@
复制 OMEMO 指纹到剪贴板
重新生成 OMEMO 密钥
清除设备
- 是否确定要从 OMEMO 公布中清除所有其他设备?下次连接时,设备将会重新公布,但可能不会收到在此期间发送的消息。
+ 是否确定要从 OMEMO 公布中清除所有其他设备?下次连接时,设备将会重新公布,但可能无法接收在此期间发送的消息。
此联系人没有可用密钥。 \n无法从服务器获取新密钥。也许是对方的服务器有问题?
此联系人没有可用密钥。 \n请确保双方都有在线状态订阅。
出了点问题
@@ -381,7 +381,7 @@
启用所有账号
禁用所有账号
执行操作
- 无
+ 访客
离线
被驱逐者
成员
@@ -404,7 +404,7 @@
公开频道配置
私人,仅成员
对任何参与者显示用户 XMPP 地址
- 开启频道发言审核
+ 启用频道发言审核
您没有发言权
群聊配置修改成功!
无法修改群聊配置
@@ -442,7 +442,7 @@
位置
离开私人群聊
离开公开频道
- 不信任系统 CA
+ 不要信任系统 CA
所有证书必须手动批准
移除证书
删除手动批准的证书
@@ -470,7 +470,7 @@
下载失败:文件无效
无法连接到 Tor 网络
绑定失败
- 域名未响应
+ 不对域名负责
损坏
在线状态
设备锁定时离开
@@ -535,25 +535,25 @@
更正消息
发送更正后的消息
您已信任此人的指纹。选择“完成”即表示您确认 %s 是此群聊的成员。
- 您禁用了此账号
+ 您已禁用此账号
安全错误:文件访问无效!
未找到可以分享 URI 的应用
分享 URI…
同意并继续
- 指导您在 conversations.im 上创建账号。\n选择 conversations.im 作为提供者时,向别人提供您的完整 XMPP 地址,就能和对方交流。
+ conversations.im 账号创建引导流程。\n当选择 conversations.im 作为服务提供者时,您只需向其他服务提供者的用户提供您的完整 XMPP 地址,即可与对方互通消息。
您的完整 XMPP 地址将是:%s
创建账号
使用我自己的提供者
选择您的用户名
手动更改在线状态
- 在编辑状态信息时,让您的联系人知道您是否可以聊天。
- 状态信息
+ 在编辑状态消息时设置您的在线状态。
+ 状态消息
有空聊天
在线
离开
没空
忙碌
- 安全密码已生成
+ 已生成安全密码
您的设备不支持选择退出电池优化
注册失败:请稍后重试
注册失败:密码太弱
@@ -593,10 +593,10 @@
重新生成 OMEMO 密钥。您的所有联系人将必须再次验证您。仅将此作为最后的方法。
删除所选密钥
连接后才能发布头像。
- 显示出错信息
- 出错信息
+ 显示错误消息
+ 错误消息
流量节省程序已启用
- 操作系统正限制 %1$s 在后台时访问互联网。要接收新消息通知,应当在“流量节省程序”开启时允许 %1$s 无限制访问。 \n在可能的情况下,%1$s 仍会尽可能节省数据。
+ 您的操作系统正在限制 %1$s 在后台时访问互联网。要接收新消息通知,应当在“流量节省程序”开启时允许 %1$s 无限制访问。\n%1$s 仍会在可能的情况下尽可能节省数据。
设备不支持为 %1$s 禁用流量节省程序。
无法创建临时文件
此设备已经过验证
@@ -609,22 +609,22 @@
以 XMPP URI 形式分享
以 HTTP 链接形式分享
验证前盲目信任
- 信任未经验证的联系人的新设备,但提示手动确认已验证的联系人的新设备。
- 盲目信任的 OMEMO 密钥,这意味着它们可能是别人的,可能会有人窃听。
+ 自动信任未经验证的联系人的新设备,但已验证的联系人的新设备需手动确认。
+ 盲目信任的 OMEMO 密钥,意味着存在密钥冒用或第三方窃听的风险。
未受信任
二维码无效
清理缓存文件夹(由相机使用)
清理缓存
清理私人存储空间
清理保存文件的私人存储(可从服务器重新下载)
- 我从可信来源获得此链接
- 点击链接后,您将验证 %1$s 的 OMEMO 密钥。只有从可信来源(只有 %2$s 可以发布此链接)获得此链接才是安全的。
- 您将验证自己账号的 OMEMO 密钥。只有从可信来源(只有您可以发布此链接)获得此链接才是安全的。
+ 我从可信来源访问此链接
+ 点击链接后,您即将验证 %1$s 的 OMEMO 密钥。只有从可信来源(只有 %2$s 可以发布此链接)访问此链接才是安全的。
+ 您即将验证自己账号的 OMEMO 密钥。只有从可信来源(只有您可以发布此链接)访问此链接才是安全的。
继续
验证 OMEMO 密钥
显示非活动设备
隐藏非活动设备
- 不再信任设备
+ 不信任设备
是否确定要移除此设备的验证?\n此设备及其消息将标记为“未受信任”。
- %d 秒
@@ -685,24 +685,23 @@
需要访问相机来扫描二维码
滚动至底部
发送消息后向下滚屏
- 编辑状态信息
- 编辑状态信息
+ 编辑状态消息
+ 编辑状态消息
禁用加密
- %1$s 无法向 %2$s 发送加密消息。可能是由于您的联系人使用了无法处理 OMEMO 的过时服务器或客户端。
+ %1$s 无法向 %2$s 发送加密消息。可能是由于对方使用了无法处理 OMEMO 的过时服务器或客户端。
无法获取设备列表
无法获取密钥
提示:某些情况下,双方可以添加到联系人列表解决此问题。
- 是否确定要禁用此对话的 OMEMO 加密?
-\n将允许服务器管理员读取您的消息,但可能是与使用过时客户端的用户交流的唯一方法。
+ 是否确定要禁用此对话的 OMEMO 加密?\n这将允许服务器管理员读取您的消息,但可能是与使用过时客户端的用户交流的唯一方式。
立即禁用
草稿:
OMEMO 加密
OMEMO 将始终用于一对一聊天和私人群聊。
新对话将默认使用 OMEMO。
- 新对话必须明确开启 OMEMO。
+ 新对话必须手动启用 OMEMO。
创建快捷方式
- 默认开启
- 默认关闭
+ 默认启用
+ 默认禁用
未对此设备加密消息。
无法解密 OMEMO 消息。
撤销
@@ -820,12 +819,12 @@
电子书
未压缩(原始)
打开…
- Conversations 个人资料照片
+ 头像
选择账号
恢复备份
恢复
请输入 %s 的密码以恢复备份。
- 请勿使用恢复备份功能尝试克隆(同时运行)安装。恢复备份仅适用于迁移或您丢失原始设备的情况。
+ 请勿通过恢复 OMEMO 密钥尝试克隆(同时运行)安装。恢复 OMEMO 密钥仅适用于迁移或您丢失原始设备的情况。
无法恢复备份。
无法解密备份。密码是否正确?
备份和恢复
@@ -843,15 +842,15 @@
此频道已存在
您已加入现有的频道
无法保存频道配置
- 允许参与者编辑话题
+ 允许参与者编辑主题
允许参与者邀请他人
- 参与者可以编辑话题。
- 所有者可以编辑话题。
- 管理员可以编辑话题。
+ 参与者可以编辑主题。
+ 所有者可以编辑主题。
+ 管理员可以编辑主题。
所有者可以邀请他人。
参与者可以邀请他人。
- 管理员可以看到用户 XMPP 地址。
- 任何参与者可以看到用户 XMPP 地址。
+ 管理员可以查看用户 XMPP 地址。
+ 任何参与者可以查看用户 XMPP 地址。
此公开频道无参与者。邀请联系人或使用分享按钮分发频道的 XMPP 地址。
此私人群聊无参与者。
管理权限
@@ -951,7 +950,7 @@
- 查看 %1$d 位参与者
- - 一些消息发送失败
+ - 部分消息无法成功发送
发送失败
更多选项
@@ -983,14 +982,14 @@
群聊
搜索群聊
从服务器移除账号
- 请勿尝试恢复您尚未自行创建的备份!
+ 仅恢复您亲自创建的备份。
您正尝试导入过时的备份文件格式
有声读物
在其他主机上重新连接
- 您登出了此账号
- 登入
+ 您已登出此账号
+ 登录
隐藏通知
- 您的联系人使用未经验证的设备。扫描对方二维码进行验证并阻止主动式中间人攻击。
+ 对方使用未经验证的设备。扫描其二维码进行验证并阻止主动式中间人攻击。
登出
已登出
您正在使用未经验证的设备。扫描您其他设备的二维码进行验证并阻止主动式中间人攻击。
@@ -1062,7 +1061,7 @@
创建一次性备份
定期备份
全屏通知
- 当设备锁定时,允许此应用显示占据全屏的来电通知。
+ 允许此应用在设备锁定时显示占据整个屏幕的来电通知。
不支持的操作
创建一次性备份、设置定期备份
允许私信
@@ -1070,7 +1069,7 @@
您的头像。点按即可从图库选择新头像。
无法禁用视频。
删除 OpenPGP 密钥
- 编辑名称和话题
+ 编辑名称和主题
更改配置
更改通知设置
正在使用听筒进行通话。
@@ -1084,27 +1083,39 @@
正在使用听筒进行通话,点按即可切换到扬声器。
登录机制
XEP-0386:绑定 2
- XEP-0388:可扩展 SASL 配置文件
+ XEP-0388:可扩展 SASL 配置
无法添加回应
添加回应…
更多回应
添加回应
无法修改通话
- 您的联系人的 XMPP 客户端可能不支持音频/视频通话。
+ 对方的 XMPP 客户端可能不支持音频/视频通话。
显示头像
背景颜色、字体大小、头像
消息气泡
消息气泡
- 在群聊和一对一聊天中为消息显示头像。
+ 在群聊和一对一聊天中,在消息旁显示头像。
通话集成
自定义通知
是否为此对话启用自定义通知(重要程度、声音、振动)设置?
此应用的通话与常规通话交互,例如另一个通话开始时结束一个通话。
在左侧显示所有消息,包括发送的消息,以实现统一的聊天布局。
左对齐消息
- 是否删除头像?某些客户端可能会继续显示您头像的缓存副本。
- 仅显示给联系人
+ 是否删除头像?部分客户端可能会继续显示已缓存的头像副本。
+ 仅对联系人显示
连接超时
使用 P2P 重试
不支持通道绑定
+ Word 文档
+ 恢复 OMEMO 密钥
+ Quicksy 只能恢复 quicksy.im 账号的备份
+ 备份位置
+ URI 已复制到剪贴板
+ 复制电子邮件地址
+ 电子邮件地址已复制到剪贴板
+ 电话号码已复制到剪贴板
+ 复制地理位置
+ URI
+ 复制 URI
+ 复制电话号码
diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml
index 05444316e1f0a7e34ffd26f11fb45d67f9192de2..4cc345b865868d8532bd11b52921b7f9085aba51 100644
--- a/src/main/res/values-zh-rTW/strings.xml
+++ b/src/main/res/values-zh-rTW/strings.xml
@@ -432,7 +432,7 @@
下載失敗:無效的檔案
Tor 網路無法使用
繫結失敗
- 伺服器無法回應此網域
+ 無法回應網域
已損毀
可用性
裝置上鎖時離開
@@ -1099,4 +1099,22 @@
呼叫正在使用揚聲器。
無法添加回應
添加回應…
+ 連接超時
+ Word 文件
+ 自訂通知
+ 使用 P2P 重試
+ 通道綁定不可用
+ 為此對話啟用自定義通知設定(重要性、聲音、振動)設定?
+ 您聯絡人的 XMPP 用戶端可能不支援音訊/視訊通話。
+ 左對齊訊息
+ 在左側顯示所有訊息,包括已傳送的訊息,以實現統一的聊天佈局。
+ 呼叫集成
+ 來自此應用程式的呼叫與常規電話交互,例如在另一個呼叫開始時結束一個呼叫。
+ 是否要刪除您的大頭貼?某些用戶端可能會繼續顯示您的大頭貼的緩存副本。
+ 僅向聯絡人顯示
+ 聊天氣泡
+ 背景顏色、字體大小、大頭貼
+ 聊天氣泡
+ 顯示大頭貼
+ 除了群聊之外,還可以顯示您的訊息和 1 對 1 聊天的大頭貼。
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 86a383855a2ce47f29e442b6d57d5d8d98e8b829..6687dee4f60dbd5a96e8fc943ddef238676e0bdb 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -321,6 +321,8 @@
Retry with P2P
File URL
Copied URL to clipboard
+ Copied URI to clipboard
+ URI
Copied Jabber ID to clipboard
Copied error message to clipboard
web address
@@ -333,7 +335,7 @@
Foreground service
Prevents the operating system from killing your connection
Create backup
- Backup files will be stored in %s
+ Backups will be stored in %s
Creating backup files
Your backup has been created
The backup files have been stored in %s
@@ -424,6 +426,7 @@
vector graphic
multimedia file
PDF document
+ Word document
Android App
Audiobook
Contact
@@ -753,6 +756,12 @@
Use the Share Location Plugin instead of the built-in map
Copy web address
Copy Jabber ID
+ Copy URI
+ Copy phone number
+ Copy geo location
+ Copy email address
+ Copied email address to clipboard
+ Copied phone number to clipboard
HTTP File Sharing for S3
Direct Search
At ‘New chat’ screen open keyboard and place cursor in search field
@@ -848,13 +857,14 @@
e-book
Original (uncompressed)
Open with…
- Conversations profile picture
+ Avatar
Choose account
Restore backup
Restore
Enter your password for the account %s to restore the backup.
- Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device.
- Do not attempt to restore backups that you have not created yourself!
+ Restore OMEMO keys
+ Do not restore OMEMO keys in an attempt to clone (run simultaneously) an installation. Restoring OMEMO keys is only meant for migrations or in case you’ve lost the original device.
+ Only restore backups you’ve personally created.
Could not restore backup.
Could not decrypt backup. Is the password correct?
Backup & Restore
@@ -904,6 +914,7 @@
Open backup
The file you selected is not a Conversations backup file
You are trying to import an outdated backup file format
+ Quicksy can only restore backups for quicksy.im accounts
This account has already been setup
Please enter the password for this account
Could not perform this action
@@ -1115,4 +1126,5 @@
Enable customized notification settings (importance, sound, vibration) settings for this conversation?
Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar.
Show to contacts only
+ Backup location
diff --git a/src/main/res/xml/preferences_backup.xml b/src/main/res/xml/preferences_backup.xml
index 32d66261e3b04c7419a8f45096ce9fdeee10f90e..888e324ce493b255b460c297cac36a30706068ec 100644
--- a/src/main/res/xml/preferences_backup.xml
+++ b/src/main/res/xml/preferences_backup.xml
@@ -26,5 +26,9 @@
+ android:icon="@drawable/ic_folder_open_24dp"
+ android:key="backup_location"
+ android:summary="@string/pref_create_backup_summary"
+ android:title="@string/pref_backup_location" />
diff --git a/src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt b/src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b7ece76fdd4e02268b145fd3a6a90ed35d9747b1
--- /dev/null
+++ b/src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt
@@ -0,0 +1,38 @@
+קל לשימוש, אמין, ידידותי לסוללה. עם תמיכה מובנית בתמונות, צ'אטים קבוצתיים והצפנת e2e.
+
+עקרונות עיצוב:
+
+* היה כמה שיותר יפה וקל לשימוש מבלי לוותר על אבטחה או פרטיות
+* הסתמכו על פרוטוקולים קיימים ומבוססים היטב
+* אין צורך בחשבון Google או ספציפית Google Cloud Messaging (GCM)
+* דרוש כמה שפחות הרשאות
+
+תכונות:
+
+* הצפנה מקצה לקצה באמצעות OMEMO או OpenPGP
+* שליחת וקבלת תמונות
+* שיחות שמע ווידאו מוצפנות (DTLS-SRTP)
+* ממשק משתמש אינטואיטיבי העומד בהנחיות לעיצוב אנדרואיד
+* תמונות / אווטארים עבור אנשי הקשר שלך
+* מסתנכרן עם לקוח שולחן העבודה * ועידות (עם תמיכה בסימניות)
+* שילוב ספר כתובות
+* מספר חשבונות / תיבת דואר נכנס מאוחדת
+* השפעה נמוכה מאוד על חיי הסוללה
+
+Conversations מקלה מאוד על יצירת חשבון בשרת conversations.im החינמי. עם זאת, שיחות יעבדו גם עם כל שרת XMPP אחר. שרתי XMPP רבים מנוהלים על ידי מתנדבים והם ללא תשלום.
+
+תכונות XMPP:
+
+Conversations עובדות עם כל שרת XMPP בחוץ. עם זאת XMPP הוא פרוטוקול הניתן להרחבה. הרחבות אלה סטנדרטיות גם במה שנקרא XEP's. שיחות תומכות בכמה כאלה כדי לשפר את חווית המשתמש הכוללת. יש סיכוי ששרת ה-XMPP הנוכחי שלך אינו תומך בהרחבות אלו. כן כדי להפיק את המרב משיחות, עליך לשקול לעבור לשרת XMPP שעושה זאת או - אפילו טוב יותר - להפעיל שרת XMPP משלך עבורך ועבור חבריך.
+
+XEPs אלה הם - נכון לעכשיו:
+
+* XEP-0065: SOCKS5 Bytestreams (או mod_proxy65). ישמש להעברת קבצים אם שני הצדדים נמצאים מאחורי חומת אש או NAT.
+* XEP-0163: פרוטוקול אירועים אישיים לאוואטרים
+* XEP-0191: פקודת חסימה מאפשרת לך לרשום שולחי דואר זבל או לחסום אנשי קשר מבלי להסיר אותם מהסגל שלך.
+* XEP-0198: ניהול זרמים מאפשר ל-XMPP לשרוד הפסקות רשת קטנות ושינויים בחיבור ה-TCP הבסיסי.
+* XEP-0280: Message Carbons שמסנכרן אוטומטית את ההודעות שאתה שולח ללקוח שולחן העבודה שלך ובכך מאפשר לך לעבור בצורה חלקה מהלקוח הנייד שלך ללקוח שולחן העבודה שלך ובחזרה תוך שיחה אחת.
+* XEP-0237: גרסת רוסטר בעיקר כדי לחסוך ברוחב פס בחיבורים ניידים גרועים
+* XEP-0313: ניהול ארכיון הודעות סנכרן את היסטוריית ההודעות עם השרת. התעדכן בהודעות שנשלחו בזמן ששיחות היו במצב לא מקוון.
+* XEP-0352: חיווי מצב לקוח מאפשר לשרת לדעת אם שיחות נמצאות ברקע או לא. מאפשר לשרת לחסוך ברוחב פס על ידי מניעת חבילות לא חשובות.
+* XEP-0363: העלאת קבצי HTTP מאפשרת לך לשתף קבצים בוועידות ועם אנשי קשר לא מקוונים. דורש רכיב נוסף בשרת שלך.
diff --git a/src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt b/src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7f96c680a2eef1412811cf4472b187953ed3ae48
--- /dev/null
+++ b/src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt
@@ -0,0 +1 @@
+Jabber/XMPP עם כניסה קלה וגילוי קל
diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java
index d2af65f8346c4d0f70c5f4572d6330f08180a482..54cfc5ee60f7357956aeb584b9cff6cdd8df4272 100644
--- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java
+++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java
@@ -1,21 +1,17 @@
package eu.siacs.conversations.services;
-
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
-
import com.google.common.collect.ImmutableMap;
-
+import de.gultsch.common.TrustManagers;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.android.PhoneNumberContact;
-import eu.siacs.conversations.crypto.TrustManagers;
import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@@ -30,11 +26,8 @@ import eu.siacs.conversations.utils.TLSSocketFactory;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
-
import im.conversations.android.xmpp.model.stanza.Iq;
-
import io.michaelrocks.libphonenumber.android.Phonenumber;
-
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
@@ -63,7 +56,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
@@ -73,7 +65,6 @@ import javax.net.ssl.X509TrustManager;
public class QuickConversationsService extends AbstractQuickConversationsService {
-
public static final int API_ERROR_OTHER = -1;
public static final int API_ERROR_UNKNOWN_HOST = -2;
public static final int API_ERROR_CONNECT = -3;
@@ -87,8 +78,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private static final String BASE_URL = "https://" + API_DOMAIN;
- private final Set mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
- private final Set mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
+ private final Set mOnVerificationRequested =
+ Collections.newSetFromMap(new WeakHashMap<>());
+ private final Set mOnVerification =
+ Collections.newSetFromMap(new WeakHashMap<>());
private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
@@ -97,7 +90,8 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private Attempt mLastSyncAttempt = Attempt.NULL;
- private final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
+ private final SerialSingleThreadExecutor mSerialSingleThreadExecutor =
+ new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
QuickConversationsService(XmppConnectionService xmppConnectionService) {
super(xmppConnectionService);
@@ -105,19 +99,22 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private static long retryAfter(HttpURLConnection connection) {
try {
- return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
+ return SystemClock.elapsedRealtime()
+ + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
} catch (Exception e) {
return 0;
}
}
- public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
+ public void addOnVerificationRequestedListener(
+ OnVerificationRequested onVerificationRequested) {
synchronized (mOnVerificationRequested) {
mOnVerificationRequested.add(onVerificationRequested);
}
}
- public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
+ public void removeOnVerificationRequestedListener(
+ OnVerificationRequested onVerificationRequested) {
synchronized (mOnVerificationRequested) {
mOnVerificationRequested.remove(onVerificationRequested);
}
@@ -139,62 +136,63 @@ public class QuickConversationsService extends AbstractQuickConversationsService
final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
if (mVerificationRequestInProgress.compareAndSet(false, true)) {
SmsRetrieverWrapper.start(service);
- new Thread(() -> {
- try {
- final URL url = new URL(BASE_URL + "/authentication/" + e164);
- final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- setBundledLetsEncrypt(service, connection);
- connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
- connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
- setHeader(connection);
- final int code = connection.getResponseCode();
- if (code == 200) {
- createAccountAndWait(phoneNumber, 0L);
- } else if (code == 429) {
- createAccountAndWait(phoneNumber, retryAfter(connection));
- } else {
- synchronized (mOnVerificationRequested) {
- for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
- onVerificationRequested.onVerificationRequestFailed(code);
- }
- }
- }
- } catch (IOException e) {
- final int code = getApiErrorCode(e);
- synchronized (mOnVerificationRequested) {
- for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
- onVerificationRequested.onVerificationRequestFailed(code);
- }
- }
- } finally {
- mVerificationRequestInProgress.set(false);
- }
- }).start();
+ new Thread(
+ () -> {
+ try {
+ final URL url = new URL(BASE_URL + "/authentication/" + e164);
+ final HttpURLConnection connection =
+ (HttpURLConnection) url.openConnection();
+ setBundledLetsEncrypt(service, connection);
+ connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
+ connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
+ setHeader(connection);
+ final int code = connection.getResponseCode();
+ if (code == 200) {
+ createAccountAndWait(phoneNumber, 0L);
+ } else if (code == 429) {
+ createAccountAndWait(phoneNumber, retryAfter(connection));
+ } else {
+ synchronized (mOnVerificationRequested) {
+ for (OnVerificationRequested onVerificationRequested :
+ mOnVerificationRequested) {
+ onVerificationRequested.onVerificationRequestFailed(
+ code);
+ }
+ }
+ }
+ } catch (IOException e) {
+ final int code = getApiErrorCode(e);
+ synchronized (mOnVerificationRequested) {
+ for (OnVerificationRequested onVerificationRequested :
+ mOnVerificationRequested) {
+ onVerificationRequested.onVerificationRequestFailed(
+ code);
+ }
+ }
+ } finally {
+ mVerificationRequestInProgress.set(false);
+ }
+ })
+ .start();
}
}
private static void setBundledLetsEncrypt(
final Context context, final HttpURLConnection connection) {
if (connection instanceof HttpsURLConnection httpsURLConnection) {
- final X509TrustManager trustManager;
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
- try {
- trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
- } catch (final NoSuchAlgorithmException
- | KeyStoreException
- | CertificateException
- | IOException e) {
- Log.e(Config.LOGTAG, "could not configured bundled LetsEncrypt", e);
- return;
- }
- } else {
- return;
- }
final SSLSocketFactory socketFactory;
try {
socketFactory =
- new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
- } catch (final KeyManagementException | NoSuchAlgorithmException e) {
+ new TLSSocketFactory(
+ new X509TrustManager[] {
+ TrustManagers.createForAndroidVersion(context)
+ },
+ SECURE_RANDOM);
+ } catch (final KeyManagementException
+ | NoSuchAlgorithmException
+ | KeyStoreException
+ | CertificateException
+ | IOException e) {
Log.e(Config.LOGTAG, "could not configured bundled LetsEncrypt", e);
return;
}
@@ -211,7 +209,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
- Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
+ Log.d(
+ Config.LOGTAG,
+ "requesting verification for "
+ + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, null);
Account account = AccountUtils.getFirst(service);
if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
@@ -237,64 +238,74 @@ public class QuickConversationsService extends AbstractQuickConversationsService
public void verify(final Account account, String pin) {
if (mVerificationInProgress.compareAndSet(false, true)) {
- new Thread(() -> {
- try {
- final URL url = new URL(BASE_URL + "/password");
- final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- setBundledLetsEncrypt(service, connection);
- connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
- connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
- connection.setRequestMethod("POST");
- connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
- setHeader(connection);
- final OutputStream os = connection.getOutputStream();
- final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
- writer.write(account.getPassword());
- writer.flush();
- writer.close();
- os.close();
- connection.connect();
- final int code = connection.getResponseCode();
- if (code == 200 || code == 201) {
- account.setOption(Account.OPTION_UNVERIFIED, false);
- account.setOption(Account.OPTION_DISABLED, false);
- awaitingAccountStateChange = new CountDownLatch(1);
- service.updateAccount(account);
- try {
- awaitingAccountStateChange.await(5, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": timer expired while waiting for account to connect");
- }
- synchronized (mOnVerification) {
- for (OnVerification onVerification : mOnVerification) {
- onVerification.onVerificationSucceeded();
- }
- }
- } else if (code == 429) {
- final long retryAfter = retryAfter(connection);
- synchronized (mOnVerification) {
- for (OnVerification onVerification : mOnVerification) {
- onVerification.onVerificationRetryAt(retryAfter);
- }
- }
- } else {
- synchronized (mOnVerification) {
- for (OnVerification onVerification : mOnVerification) {
- onVerification.onVerificationFailed(code);
- }
- }
- }
- } catch (IOException e) {
- final int code = getApiErrorCode(e);
- synchronized (mOnVerification) {
- for (OnVerification onVerification : mOnVerification) {
- onVerification.onVerificationFailed(code);
- }
- }
- } finally {
- mVerificationInProgress.set(false);
- }
- }).start();
+ new Thread(
+ () -> {
+ try {
+ final URL url = new URL(BASE_URL + "/password");
+ final HttpURLConnection connection =
+ (HttpURLConnection) url.openConnection();
+ setBundledLetsEncrypt(service, connection);
+ connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
+ connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
+ connection.setRequestMethod("POST");
+ connection.setRequestProperty(
+ "Authorization",
+ Plain.getMessage(account.getUsername(), pin));
+ setHeader(connection);
+ final OutputStream os = connection.getOutputStream();
+ final BufferedWriter writer =
+ new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+ writer.write(account.getPassword());
+ writer.flush();
+ writer.close();
+ os.close();
+ connection.connect();
+ final int code = connection.getResponseCode();
+ if (code == 200 || code == 201) {
+ account.setOption(Account.OPTION_UNVERIFIED, false);
+ account.setOption(Account.OPTION_DISABLED, false);
+ awaitingAccountStateChange = new CountDownLatch(1);
+ service.updateAccount(account);
+ try {
+ awaitingAccountStateChange.await(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": timer expired while waiting for"
+ + " account to connect");
+ }
+ synchronized (mOnVerification) {
+ for (OnVerification onVerification : mOnVerification) {
+ onVerification.onVerificationSucceeded();
+ }
+ }
+ } else if (code == 429) {
+ final long retryAfter = retryAfter(connection);
+ synchronized (mOnVerification) {
+ for (OnVerification onVerification : mOnVerification) {
+ onVerification.onVerificationRetryAt(retryAfter);
+ }
+ }
+ } else {
+ synchronized (mOnVerification) {
+ for (OnVerification onVerification : mOnVerification) {
+ onVerification.onVerificationFailed(code);
+ }
+ }
+ }
+ } catch (IOException e) {
+ final int code = getApiErrorCode(e);
+ synchronized (mOnVerification) {
+ for (OnVerification onVerification : mOnVerification) {
+ onVerification.onVerificationFailed(code);
+ }
+ }
+ } finally {
+ mVerificationInProgress.set(false);
+ }
+ })
+ .start();
}
}
@@ -339,7 +350,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService
return mVerificationRequestInProgress.get();
}
-
@Override
public boolean isSynchronizing() {
return mRunningSyncJobs.get() > 0;
@@ -353,12 +363,13 @@ public class QuickConversationsService extends AbstractQuickConversationsService
@Override
public void considerSyncBackground(final boolean forced) {
mRunningSyncJobs.incrementAndGet();
- mSerialSingleThreadExecutor.execute(() -> {
- considerSync(forced);
- if (mRunningSyncJobs.decrementAndGet() == 0) {
- service.updateRosterUi();
- }
- });
+ mSerialSingleThreadExecutor.execute(
+ () -> {
+ considerSync(forced);
+ if (mRunningSyncJobs.decrementAndGet() == 0) {
+ service.updateRosterUi();
+ }
+ });
}
@Override
@@ -380,16 +391,19 @@ public class QuickConversationsService extends AbstractQuickConversationsService
onVerification.startBackgroundVerification(pin);
}
}
-
}
-
private void considerSync(boolean forced) {
- final ImmutableMap allContacts = PhoneNumberContact.load(service);
+ final ImmutableMap allContacts =
+ PhoneNumberContact.load(service);
for (final Account account : service.getAccounts()) {
- final Map contacts = filtered(allContacts, account.getJid().getLocal());
+ final Map contacts =
+ filtered(allContacts, account.getJid().getLocal());
if (contacts.size() < allContacts.size()) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": found own phone number in address book. ignoring...");
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": found own phone number in address book. ignoring...");
}
refresh(account, contacts.values());
if (!considerSync(account, contacts, forced)) {
@@ -408,17 +422,24 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
private void refresh(Account account, Collection contacts) {
- for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
+ for (Contact contact :
+ account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
final Uri uri = contact.getSystemAccount();
if (uri == null) {
continue;
}
final String number = getNumber(contact);
- final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(contacts, uri, number);
+ final PhoneNumberContact phoneNumberContact =
+ PhoneNumberContact.findByUriOrNumber(contacts, uri, number);
final boolean needsCacheClean;
if (phoneNumberContact != null) {
if (!uri.equals(phoneNumberContact.getLookupUri())) {
- Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri());
+ Log.d(
+ Config.LOGTAG,
+ "lookupUri has changed from "
+ + uri
+ + " to "
+ + phoneNumberContact.getLookupUri());
}
needsCacheClean = contact.setPhoneContact(phoneNumberContact);
} else {
@@ -439,7 +460,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
return null;
}
- private boolean considerSync(final Account account, final Map contacts, final boolean forced) {
+ private boolean considerSync(
+ final Account account,
+ final Map contacts,
+ final boolean forced) {
final int hash = contacts.keySet().hashCode();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
if (!mLastSyncAttempt.retry(hash) && !forced) {
@@ -448,59 +472,79 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
mRunningSyncJobs.incrementAndGet();
final Jid syncServer = Jid.of(API_DOMAIN);
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending phone list to " + syncServer);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": sending phone list to " + syncServer);
final List entries = new ArrayList<>();
for (final PhoneNumberContact c : contacts.values()) {
entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber()));
}
final Iq query = new Iq(Iq.Type.GET);
query.setTo(syncServer);
- final Element book = new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries);
- final String statusQuo = Entry.statusQuo(contacts.values(), account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
+ final Element book =
+ new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries);
+ final String statusQuo =
+ Entry.statusQuo(
+ contacts.values(),
+ account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
book.setAttribute("ver", statusQuo);
query.addChild(book);
mLastSyncAttempt = Attempt.create(hash);
- service.sendIqPacket(account, query, (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION);
- if (phoneBook != null) {
- final List withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
- for (Entry entry : Entry.ofPhoneBook(phoneBook)) {
- final PhoneNumberContact phoneContact = contacts.get(entry.getNumber());
- if (phoneContact == null) {
- continue;
- }
- for (final Jid jid : entry.getJids()) {
- final Contact contact = account.getRoster().getContact(jid);
- final boolean needsCacheClean = contact.setPhoneContact(phoneContact);
- if (needsCacheClean) {
- service.getAvatarService().clear(contact);
+ service.sendIqPacket(
+ account,
+ query,
+ (response) -> {
+ if (response.getType() == Iq.Type.RESULT) {
+ final Element phoneBook =
+ response.findChild("phone-book", Namespace.SYNCHRONIZATION);
+ if (phoneBook != null) {
+ final List withSystemAccounts =
+ account.getRoster()
+ .getWithSystemAccounts(PhoneNumberContact.class);
+ for (Entry entry : Entry.ofPhoneBook(phoneBook)) {
+ final PhoneNumberContact phoneContact =
+ contacts.get(entry.getNumber());
+ if (phoneContact == null) {
+ continue;
+ }
+ for (final Jid jid : entry.getJids()) {
+ final Contact contact = account.getRoster().getContact(jid);
+ final boolean needsCacheClean =
+ contact.setPhoneContact(phoneContact);
+ if (needsCacheClean) {
+ service.getAvatarService().clear(contact);
+ }
+ withSystemAccounts.remove(contact);
+ }
}
- withSystemAccounts.remove(contact);
- }
- }
- for (final Contact contact : withSystemAccounts) {
- final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
- if (needsCacheClean) {
- service.getAvatarService().clear(contact);
+ for (final Contact contact : withSystemAccounts) {
+ final boolean needsCacheClean =
+ contact.unsetPhoneContact(PhoneNumberContact.class);
+ if (needsCacheClean) {
+ service.getAvatarService().clear(contact);
+ }
+ }
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": phone number contact list remains unchanged");
}
+ } else if (response.getType() == Iq.Type.TIMEOUT) {
+ mLastSyncAttempt = Attempt.NULL;
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": failed to sync contact list with api server");
}
- } else {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": phone number contact list remains unchanged");
- }
- } else if (response.getType() == Iq.Type.TIMEOUT) {
- mLastSyncAttempt = Attempt.NULL;
- } else {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to sync contact list with api server");
- }
- mRunningSyncJobs.decrementAndGet();
- service.syncRoster(account);
- service.updateRosterUi();
- });
+ mRunningSyncJobs.decrementAndGet();
+ service.syncRoster(account);
+ service.updateRosterUi();
+ });
return true;
}
-
public interface OnVerificationRequested {
void onVerificationRequestFailed(int code);
@@ -535,7 +579,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
public boolean retry(int hash) {
- return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL;
+ return hash != this.hash
+ || SystemClock.elapsedRealtime() - timestamp
+ >= Config.CONTACT_SYNC_RETRY_INTERVAL;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java
index a1f55e2b4d77de17d01fbd6be2bf6a33d9dd4306..21753de3850b1237b7aa894fc8bb99a4d915c8c1 100644
--- a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java
+++ b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java
@@ -1,10 +1,10 @@
package eu.siacs.conversations.ui;
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+
+import android.Manifest;
import android.app.AlertDialog;
import android.content.Intent;
-
-import androidx.annotation.NonNull;
-import androidx.databinding.DataBindingUtil;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
@@ -12,11 +12,13 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityEnterNumberBinding;
@@ -30,57 +32,73 @@ import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
import io.michaelrocks.libphonenumber.android.Phonenumber;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
-public class EnterPhoneNumberActivity extends XmppActivity implements QuickConversationsService.OnVerificationRequested {
+public class EnterPhoneNumberActivity extends XmppActivity
+ implements QuickConversationsService.OnVerificationRequested {
private static final int REQUEST_CHOOSE_COUNTRY = 0x1234;
+ private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
private ActivityEnterNumberBinding binding;
private final AtomicBoolean redirectInProgress = new AtomicBoolean(false);
private String region = null;
- private final TextWatcher countryCodeTextWatcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
-
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- final String text = editable.toString();
- try {
- final int oldCode = region != null ? PhoneNumberUtilWrapper.getInstance(EnterPhoneNumberActivity.this).getCountryCodeForRegion(region) : 0;
- final int code = Integer.parseInt(text);
- if (oldCode != code) {
- region = PhoneNumberUtilWrapper.getInstance(EnterPhoneNumberActivity.this).getRegionCodeForCountryCode(code);
+ private final TextWatcher countryCodeTextWatcher =
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ final String text = editable.toString();
+ try {
+ final int oldCode =
+ region != null
+ ? PhoneNumberUtilWrapper.getInstance(
+ EnterPhoneNumberActivity.this)
+ .getCountryCodeForRegion(region)
+ : 0;
+ final int code = Integer.parseInt(text);
+ if (oldCode != code) {
+ region =
+ PhoneNumberUtilWrapper.getInstance(
+ EnterPhoneNumberActivity.this)
+ .getRegionCodeForCountryCode(code);
+ }
+ if ("ZZ".equals(region)) {
+ binding.country.setText(
+ TextUtils.isEmpty(text)
+ ? R.string.choose_a_country
+ : R.string.invalid_country_code);
+ } else {
+ binding.number.requestFocus();
+ binding.country.setText(
+ PhoneNumberUtilWrapper.getCountryForCode(region));
+ }
+ } catch (NumberFormatException e) {
+ binding.country.setText(
+ TextUtils.isEmpty(text)
+ ? R.string.choose_a_country
+ : R.string.invalid_country_code);
+ }
}
- if ("ZZ".equals(region)) {
- binding.country.setText(TextUtils.isEmpty(text) ? R.string.choose_a_country : R.string.invalid_country_code);
- } else {
- binding.number.requestFocus();
- binding.country.setText(PhoneNumberUtilWrapper.getCountryForCode(region));
- }
- } catch (NumberFormatException e) {
- binding.country.setText(TextUtils.isEmpty(text) ? R.string.choose_a_country : R.string.invalid_country_code);
- }
- }
- };
+ };
private boolean requestingVerification = false;
@Override
- protected void refreshUiReal() {
-
- }
+ protected void refreshUiReal() {}
@Override
public void onBackendConnected() {
- xmppConnectionService.getQuickConversationsService().addOnVerificationRequestedListener(this);
+ xmppConnectionService
+ .getQuickConversationsService()
+ .addOnVerificationRequestedListener(this);
final Account account = AccountUtils.getFirst(xmppConnectionService);
if (account != null) {
runOnUiThread(this::performRedirectToVerificationActivity);
@@ -92,7 +110,9 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
super.onCreate(savedInstanceState);
String region = savedInstanceState != null ? savedInstanceState.getString("region") : null;
- boolean requestingVerification = savedInstanceState != null && savedInstanceState.getBoolean("requesting_verification", false);
+ boolean requestingVerification =
+ savedInstanceState != null
+ && savedInstanceState.getBoolean("requesting_verification", false);
if (region != null) {
this.region = region;
} else {
@@ -100,31 +120,73 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
}
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_number);
- this.binding.countryCode.setCompoundDrawables(new TextDrawable(this.binding.countryCode, "+"), null, null, null);
+ this.binding.countryCode.setCompoundDrawables(
+ new TextDrawable(this.binding.countryCode, "+"), null, null, null);
this.binding.country.setOnClickListener(this::onSelectCountryClick);
this.binding.next.setOnClickListener(this::onNextClick);
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
setSupportActionBar(this.binding.toolbar);
this.binding.countryCode.addTextChangedListener(this.countryCodeTextWatcher);
- this.binding.countryCode.setText(String.valueOf(PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(this.region)));
- this.binding.number.setOnKeyListener((v, keyCode, event) -> {
- if (event.getAction() != KeyEvent.ACTION_DOWN) {
- return false;
+ this.binding.countryCode.setText(
+ String.valueOf(
+ PhoneNumberUtilWrapper.getInstance(this)
+ .getCountryCodeForRegion(this.region)));
+ this.binding.number.setOnKeyListener(
+ (v, keyCode, event) -> {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+ final EditText editText = (EditText) v;
+ final boolean cursorAtZero =
+ editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0;
+ if (keyCode == KeyEvent.KEYCODE_DEL
+ && (cursorAtZero || editText.getText().length() == 0)) {
+ final Editable countryCode = this.binding.countryCode.getText();
+ if (countryCode.length() > 0) {
+ countryCode.delete(countryCode.length() - 1, countryCode.length());
+ this.binding.countryCode.setSelection(countryCode.length());
+ }
+ this.binding.countryCode.requestFocus();
+ return true;
+ }
+ return false;
+ });
+ setRequestingVerificationState(requestingVerification);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.verify_phone_number_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == R.id.action_import_backup) {
+ if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+ startActivity(new Intent(this, ImportBackupActivity.class));
}
- final EditText editText = (EditText) v;
- final boolean cursorAtZero = editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0;
- if (keyCode == KeyEvent.KEYCODE_DEL && (cursorAtZero || editText.getText().length() == 0)) {
- final Editable countryCode = this.binding.countryCode.getText();
- if (countryCode.length() > 0) {
- countryCode.delete(countryCode.length() - 1, countryCode.length());
- this.binding.countryCode.setSelection(countryCode.length());
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
+ if (grantResults.length > 0) {
+ if (allGranted(grantResults)) {
+ if (requestCode == REQUEST_IMPORT_BACKUP) {
+ startActivity(new Intent(this, ImportBackupActivity.class));
}
- this.binding.countryCode.requestFocus();
- return true;
+ } else if (Arrays.asList(permissions)
+ .contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
}
- return false;
- });
- setRequestingVerificationState(requestingVerification);
+ }
}
@Override
@@ -139,7 +201,9 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
@Override
public void onStop() {
if (xmppConnectionService != null) {
- xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this);
+ xmppConnectionService
+ .getQuickConversationsService()
+ .removeOnVerificationRequestedListener(this);
}
super.onStop();
}
@@ -149,18 +213,26 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
try {
final Editable number = this.binding.number.getText();
final String input = number.toString();
- final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtilWrapper.getInstance(this).parse(input, region);
+ final Phonenumber.PhoneNumber phoneNumber =
+ PhoneNumberUtilWrapper.getInstance(this).parse(input, region);
this.binding.countryCode.setText(String.valueOf(phoneNumber.getCountryCode()));
number.clear();
number.append(String.valueOf(phoneNumber.getNationalNumber()));
- final String formattedPhoneNumber = PhoneNumberUtilWrapper.getInstance(this).format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL).replace(' ','\u202F');
+ final String formattedPhoneNumber =
+ PhoneNumberUtilWrapper.getInstance(this)
+ .format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
+ .replace(' ', '\u202F');
if (PhoneNumberUtilWrapper.getInstance(this).isValidNumber(phoneNumber)) {
- builder.setMessage(Html.fromHtml(getString(R.string.we_will_be_verifying, formattedPhoneNumber)));
+ builder.setMessage(
+ Html.fromHtml(
+ getString(R.string.we_will_be_verifying, formattedPhoneNumber)));
builder.setNegativeButton(R.string.edit, null);
- builder.setPositiveButton(R.string.ok, (dialog, which) -> onPhoneNumberEntered(phoneNumber));
+ builder.setPositiveButton(
+ R.string.ok, (dialog, which) -> onPhoneNumberEntered(phoneNumber));
} else {
- builder.setMessage(getString(R.string.not_a_valid_phone_number, formattedPhoneNumber));
+ builder.setMessage(
+ getString(R.string.not_a_valid_phone_number, formattedPhoneNumber));
builder.setPositiveButton(R.string.ok, null);
}
Log.d(Config.LOGTAG, phoneNumber.toString());
@@ -199,7 +271,8 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
final String region = data.getStringExtra("region");
if (region != null) {
this.region = region;
- final int countryCode = PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(region);
+ final int countryCode =
+ PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(region);
this.binding.countryCode.setText(String.valueOf(countryCode));
}
}
@@ -223,10 +296,11 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
@Override
public void onVerificationRequestFailed(int code) {
- runOnUiThread(() -> {
- setRequestingVerificationState(false);
- ApiDialogHelper.createError(this, code).show();
- });
+ runOnUiThread(
+ () -> {
+ setRequestingVerificationState(false);
+ ApiDialogHelper.createError(this, code).show();
+ });
}
@Override
diff --git a/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java
index ae9894b47734b8700e433911a0fb8dafb243955f..728692def9afd85db0c571d45e80d6ffc73ee1d0 100644
--- a/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java
+++ b/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java
@@ -13,12 +13,9 @@ import android.os.Handler;
import android.os.SystemClock;
import android.text.Html;
import android.view.View;
-
import androidx.databinding.DataBindingUtil;
-
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
-
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityVerifyBinding;
import eu.siacs.conversations.entities.Account;
@@ -28,12 +25,13 @@ import eu.siacs.conversations.ui.util.PinEntryWrapper;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.TimeFrameUtils;
-
import io.michaelrocks.libphonenumber.android.NumberParseException;
-
import java.util.concurrent.atomic.AtomicBoolean;
-public class VerifyActivity extends XmppActivity implements ClipboardManager.OnPrimaryClipChangedListener, QuickConversationsService.OnVerification, QuickConversationsService.OnVerificationRequested {
+public class VerifyActivity extends XmppActivity
+ implements ClipboardManager.OnPrimaryClipChangedListener,
+ QuickConversationsService.OnVerification,
+ QuickConversationsService.OnVerificationRequested {
public static final String EXTRA_RETRY_SMS_AFTER = "retry_sms_after";
private static final String EXTRA_RETRY_VERIFICATION_AFTER = "retry_verification_after";
@@ -46,23 +44,25 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
private boolean verifying = false;
private boolean requestingVerification = false;
private long retrySmsAfter = 0;
- private final Runnable SMS_TIMEOUT_UPDATER = new Runnable() {
- @Override
- public void run() {
- if (setTimeoutLabelInResendButton()) {
- mHandler.postDelayed(this, 300);
- }
- }
- };
+ private final Runnable SMS_TIMEOUT_UPDATER =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (setTimeoutLabelInResendButton()) {
+ mHandler.postDelayed(this, 300);
+ }
+ }
+ };
private long retryVerificationAfter = 0;
- private final Runnable VERIFICATION_TIMEOUT_UPDATER = new Runnable() {
- @Override
- public void run() {
- if (setTimeoutLabelInNextButton()) {
- mHandler.postDelayed(this, 300);
- }
- }
- };
+ private final Runnable VERIFICATION_TIMEOUT_UPDATER =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (setTimeoutLabelInNextButton()) {
+ mHandler.postDelayed(this, 300);
+ }
+ }
+ };
private final AtomicBoolean redirectInProgress = new AtomicBoolean(false);
private boolean setTimeoutLabelInResendButton() {
@@ -70,7 +70,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
long remaining = retrySmsAfter - SystemClock.elapsedRealtime();
if (remaining >= 0) {
binding.resendSms.setEnabled(false);
- binding.resendSms.setText(getString(R.string.resend_sms_in, TimeFrameUtils.resolve(VerifyActivity.this, remaining)));
+ binding.resendSms.setText(
+ getString(
+ R.string.resend_sms_in,
+ TimeFrameUtils.resolve(VerifyActivity.this, remaining)));
return true;
}
}
@@ -84,7 +87,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
long remaining = retryVerificationAfter - SystemClock.elapsedRealtime();
if (remaining >= 0) {
binding.next.setEnabled(false);
- binding.next.setText(getString(R.string.wait_x, TimeFrameUtils.resolve(VerifyActivity.this, remaining)));
+ binding.next.setText(
+ getString(
+ R.string.wait_x,
+ TimeFrameUtils.resolve(VerifyActivity.this, remaining)));
return true;
}
}
@@ -97,11 +103,20 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String pin = savedInstanceState != null ? savedInstanceState.getString("pin") : null;
- boolean verifying = savedInstanceState != null && savedInstanceState.getBoolean("verifying");
- boolean requestingVerification = savedInstanceState != null && savedInstanceState.getBoolean("requesting_verification", false);
+ boolean verifying =
+ savedInstanceState != null && savedInstanceState.getBoolean("verifying");
+ boolean requestingVerification =
+ savedInstanceState != null
+ && savedInstanceState.getBoolean("requesting_verification", false);
this.pasted = savedInstanceState != null ? savedInstanceState.getString("pasted") : null;
- this.retrySmsAfter = savedInstanceState != null ? savedInstanceState.getLong(EXTRA_RETRY_SMS_AFTER, 0L) : 0L;
- this.retryVerificationAfter = savedInstanceState != null ? savedInstanceState.getLong(EXTRA_RETRY_VERIFICATION_AFTER, 0L) : 0L;
+ this.retrySmsAfter =
+ savedInstanceState != null
+ ? savedInstanceState.getLong(EXTRA_RETRY_SMS_AFTER, 0L)
+ : 0L;
+ this.retryVerificationAfter =
+ savedInstanceState != null
+ ? savedInstanceState.getLong(EXTRA_RETRY_VERIFICATION_AFTER, 0L)
+ : 0L;
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_verify);
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
setSupportActionBar(this.binding.toolbar);
@@ -126,11 +141,13 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
if (this.account != null) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setMessage(R.string.abort_registration_procedure);
- builder.setPositiveButton(R.string.yes, (dialog, which) -> {
- xmppConnectionService.deleteAccount(account);
- startActivity(intent);
- finish();
- });
+ builder.setPositiveButton(
+ R.string.yes,
+ (dialog, which) -> {
+ xmppConnectionService.deleteAccount(account);
+ startActivity(intent);
+ finish();
+ });
builder.setNegativeButton(R.string.no, null);
builder.create().show();
} else {
@@ -156,7 +173,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
private void onResendSmsButton(View view) {
try {
- xmppConnectionService.getQuickConversationsService().requestVerification(PhoneNumberUtilWrapper.toPhoneNumber(this, account.getJid()));
+ xmppConnectionService
+ .getQuickConversationsService()
+ .requestVerification(
+ PhoneNumberUtilWrapper.toPhoneNumber(this, account.getJid()));
setRequestingVerificationState(true);
} catch (NumberParseException e) {
@@ -182,29 +202,35 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
} else {
setTimeoutLabelInResendButton();
}
-
}
@Override
- protected void refreshUiReal() {
-
- }
+ protected void refreshUiReal() {}
@Override
public void onBackendConnected() {
xmppConnectionService.getQuickConversationsService().addOnVerificationListener(this);
- xmppConnectionService.getQuickConversationsService().addOnVerificationRequestedListener(this);
+ xmppConnectionService
+ .getQuickConversationsService()
+ .addOnVerificationRequestedListener(this);
this.account = AccountUtils.getFirst(xmppConnectionService);
if (this.account == null) {
return;
}
- if (!account.isOptionSet(Account.OPTION_UNVERIFIED) && !account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!account.isOptionSet(Account.OPTION_UNVERIFIED)
+ && !account.isOptionSet(Account.OPTION_DISABLED)) {
runOnUiThread(this::performPostVerificationRedirect);
return;
}
- this.binding.weHaveSent.setText(Html.fromHtml(getString(R.string.we_have_sent_you_an_sms_to_x, PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, this.account.getJid()))));
+ this.binding.weHaveSent.setText(
+ Html.fromHtml(
+ getString(
+ R.string.we_have_sent_you_an_sms_to_x,
+ PhoneNumberUtilWrapper.toFormattedPhoneNumber(
+ this, this.account.getJid()))));
setVerifyingState(xmppConnectionService.getQuickConversationsService().isVerifying());
- setRequestingVerificationState(xmppConnectionService.getQuickConversationsService().isRequestingVerification());
+ setRequestingVerificationState(
+ xmppConnectionService.getQuickConversationsService().isRequestingVerification());
}
@Override
@@ -225,7 +251,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
super.onStart();
clipboardManager.addPrimaryClipChangedListener(this);
final Intent intent = getIntent();
- this.retrySmsAfter = intent != null ? intent.getLongExtra(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter) : this.retrySmsAfter;
+ this.retrySmsAfter =
+ intent != null
+ ? intent.getLongExtra(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter)
+ : this.retrySmsAfter;
if (this.retrySmsAfter > 0) {
mHandler.post(SMS_TIMEOUT_UPDATER);
}
@@ -242,7 +271,9 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
clipboardManager.removePrimaryClipChangedListener(this);
if (xmppConnectionService != null) {
xmppConnectionService.getQuickConversationsService().removeOnVerificationListener(this);
- xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this);
+ xmppConnectionService
+ .getQuickConversationsService()
+ .removeOnVerificationRequestedListener(this);
}
}
@@ -250,14 +281,15 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
public void onResume() {
super.onResume();
if (pinEntryWrapper.isEmpty()) {
- //starting with Android P we need input focus
+ // starting with Android P we need input focus
pinEntryWrapper.requestFocus();
pastePinFromClipboard();
}
}
private void pastePinFromClipboard() {
- final ClipDescription description = clipboardManager != null ? clipboardManager.getPrimaryClipDescription() : null;
+ final ClipDescription description =
+ clipboardManager != null ? clipboardManager.getPrimaryClipDescription() : null;
if (description != null && description.hasMimeType(MIMETYPE_TEXT_PLAIN)) {
final ClipData primaryClip = clipboardManager.getPrimaryClip();
if (primaryClip != null && primaryClip.getItemCount() > 0) {
@@ -265,7 +297,11 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
if (PinEntryWrapper.isValidPin(clip) && !clip.toString().equals(this.pasted)) {
this.pasted = clip.toString();
pinEntryWrapper.setPin(clip.toString());
- final Snackbar snackbar = Snackbar.make(binding.coordinator, R.string.possible_pin, Snackbar.LENGTH_LONG);
+ final Snackbar snackbar =
+ Snackbar.make(
+ binding.coordinator,
+ R.string.possible_pin,
+ Snackbar.LENGTH_LONG);
snackbar.setAction(R.string.undo, v -> pinEntryWrapper.clear());
snackbar.show();
}
@@ -291,17 +327,19 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
@Override
public void onVerificationFailed(final int code) {
- runOnUiThread(() -> {
- setVerifyingState(false);
- if (code == 401 || code == 404) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setMessage(code == 404 ? R.string.pin_expired : R.string.incorrect_pin);
- builder.setPositiveButton(R.string.ok, null);
- builder.create().show();
- } else {
- ApiDialogHelper.createError(this, code).show();
- }
- });
+ runOnUiThread(
+ () -> {
+ setVerifyingState(false);
+ if (code == 401 || code == 404) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(
+ code == 404 ? R.string.pin_expired : R.string.incorrect_pin);
+ builder.setPositiveButton(R.string.ok, null);
+ builder.create().show();
+ } else {
+ ApiDialogHelper.createError(this, code).show();
+ }
+ });
}
@Override
@@ -312,10 +350,11 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
@Override
public void onVerificationRetryAt(long timestamp) {
this.retryVerificationAfter = timestamp;
- runOnUiThread(() -> {
- ApiDialogHelper.createTooManyAttempts(this).show();
- setVerifyingState(false);
- });
+ runOnUiThread(
+ () -> {
+ ApiDialogHelper.createTooManyAttempts(this).show();
+ setVerifyingState(false);
+ });
mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER);
runOnUiThread(VERIFICATION_TIMEOUT_UPDATER);
}
@@ -326,35 +365,38 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
setVerifyingState(true);
}
- //send sms again button callback
+ // send sms again button callback
@Override
public void onVerificationRequestFailed(int code) {
- runOnUiThread(() -> {
- setRequestingVerificationState(false);
- ApiDialogHelper.createError(this, code).show();
- });
+ runOnUiThread(
+ () -> {
+ setRequestingVerificationState(false);
+ ApiDialogHelper.createError(this, code).show();
+ });
}
- //send sms again button callback
+ // send sms again button callback
@Override
public void onVerificationRequested() {
- runOnUiThread(() -> {
- pinEntryWrapper.clear();
- setRequestingVerificationState(false);
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setMessage(R.string.we_have_sent_you_another_sms);
- builder.setPositiveButton(R.string.ok, null);
- builder.create().show();
- });
+ runOnUiThread(
+ () -> {
+ pinEntryWrapper.clear();
+ setRequestingVerificationState(false);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.we_have_sent_you_another_sms);
+ builder.setPositiveButton(R.string.ok, null);
+ builder.create().show();
+ });
}
@Override
public void onVerificationRequestedRetryAt(long timestamp) {
this.retrySmsAfter = timestamp;
- runOnUiThread(() -> {
- ApiDialogHelper.createRateLimited(this, timestamp).show();
- setRequestingVerificationState(false);
- });
+ runOnUiThread(
+ () -> {
+ ApiDialogHelper.createRateLimited(this, timestamp).show();
+ setRequestingVerificationState(false);
+ });
mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER);
runOnUiThread(SMS_TIMEOUT_UPDATER);
}
diff --git a/src/quicksy/res/menu/verify_phone_number_menu.xml b/src/quicksy/res/menu/verify_phone_number_menu.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fd08df4333d011723e960767bf29cc96a77e07c3
--- /dev/null
+++ b/src/quicksy/res/menu/verify_phone_number_menu.xml
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/src/quicksy/res/values-el/strings.xml b/src/quicksy/res/values-el/strings.xml
index 46c41a729470d7e96d01bef0484a897a4aa5ae82..df63fb027b27da5fbd2066fe175a7749faf1ed20 100644
--- a/src/quicksy/res/values-el/strings.xml
+++ b/src/quicksy/res/values-el/strings.xml
@@ -1,9 +1,9 @@
- Ο χρόνος σίγασης ειδοποιήσεων του Quicksy αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας.
+ Ο χρόνος σίγασης ειδοποιήσεων του Quicksy αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας
Στέλνοντας ίχνη στοίβας προωθείτε την συνεχόμενη ανάπτυξη του Quicksy
Επιτρέψτε στις επαφές σας να γνωρίζουν πότε χρησιμοποιείτε το Quicksy
- Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι σβηστή, χρειάζεται να προσθέσετε το Quicksy στον κατάλογο με τις προστατευμένες εφαρμογές.
+ Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι κλειστή, χρειάζεται να προσθέσετε το Quicksy στον κατάλογο με τις προστατευμένες εφαρμογές.
Φωτογραφία προφίλ του Quicksy
Το Quicksy δεν είναι διαθέσιμο στην χώρα σας.
Αδυναμία επαλήθευσης της ταυτότητας του διακομιστή.
diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml
index f581a993adc2c0c6195447d72e389abb96c7d721..15108394297bcd76e8bd073e873103022d8184ef 100644
--- a/src/quicksy/res/values-es/strings.xml
+++ b/src/quicksy/res/values-es/strings.xml
@@ -1,12 +1,12 @@
- El tiempo que Quicksy silencia las notificaciones tras detectar actividad en otro de tus dispositivos
+ El tiempo que Quicksy silencia las notificaciones tras detectar actividad en otro dispositivo
Al enviar informes de fallos, ayudará a desarrollar Quicksy aún más
- Informar a tus contactos cuando usas Quicksy
+ Informar a sus contactos cuando usa Quicksy
Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas.
Foto de perfil de Quicksy
- Quicksy no está disponible en tu país.
+ Quicksy no está disponible en su país.
No se ha podido verificar la identidad del servidor.
Error de seguridad desconocido.
Se ha superado el tiempo máximo de espera conectando al servidor.
-
\ No newline at end of file
+
diff --git a/src/quicksy/res/values-iw/strings.xml b/src/quicksy/res/values-iw/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7440fe9a6c5920f2c7cb9142ec82b592542e21c8
--- /dev/null
+++ b/src/quicksy/res/values-iw/strings.xml
@@ -0,0 +1,12 @@
+
+
+ משך הזמן ש-Quicksy שומרת על שקט לאחר פעילות במכשיר אחר
+ על ידי שליחת יומן קריסות אתה עוזר לפיתוח המתמשך של Quicksy
+ הודע לכל אנשי הקשר שלך כשאתה משתמש ב-Quicksy
+ כדי להמשיך לקבל התראות, גם כשהמסך כבוי, עליך להוסיף את Quicksy לרשימת האפליקציות המוגנות.
+ תמונת פרופיל של Quicksy
+ Quicksy אינו זמין במדינה שלך.
+ לא ניתן לאמת את זהות השרת.
+ שגיאת אבטחה לא ידועה.
+ פסק זמן בזמן ההתחברות לשרת.
+
diff --git a/src/quicksy/res/values-kab/strings.xml b/src/quicksy/res/values-kab/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2c625e3012fc607e7a233b8302b596ae49dbb90e
--- /dev/null
+++ b/src/quicksy/res/values-kab/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Tugna n umaɣnu n Quicksy
+ Tuccḍa n tɣellist tarussint
+ Quicksy ur yelli ara deg tmurt-nnwen.
+
diff --git a/src/test/java/de/gultsch/common/MiniUriTest.java b/src/test/java/de/gultsch/common/MiniUriTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..977fcc3710a16167c1af5811f42134a70b488e67
--- /dev/null
+++ b/src/test/java/de/gultsch/common/MiniUriTest.java
@@ -0,0 +1,62 @@
+package de.gultsch.common;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MiniUriTest {
+
+ @Test
+ public void httpsUrl() {
+ final var miniUri = new MiniUri("https://example.com");
+ Assert.assertEquals("https", miniUri.getScheme());
+ Assert.assertEquals("example.com", miniUri.getAuthority());
+ Assert.assertNull(miniUri.getPath());
+ }
+
+ @Test
+ public void httpsUrlHtml() {
+ final var miniUri = new MiniUri("https://example.com/test.html");
+ Assert.assertEquals("https", miniUri.getScheme());
+ Assert.assertEquals("example.com", miniUri.getAuthority());
+ Assert.assertEquals("/test.html", miniUri.getPath());
+ }
+
+ @Test
+ public void httpsUrlCgiFooBar() {
+ final var miniUri = new MiniUri("https://example.com/test.cgi?foo=bar");
+ Assert.assertEquals("https", miniUri.getScheme());
+ Assert.assertEquals("example.com", miniUri.getAuthority());
+ Assert.assertEquals("/test.cgi", miniUri.getPath());
+ Assert.assertEquals(ImmutableMap.of("foo", "bar"), miniUri.getParameter());
+ }
+
+ @Test
+ public void xmppUri() {
+ final var miniUri = new MiniUri("xmpp:user@example.com");
+ Assert.assertEquals("xmpp", miniUri.getScheme());
+ Assert.assertNull(miniUri.getAuthority());
+ Assert.assertEquals("user@example.com", miniUri.getPath());
+ }
+
+ @Test
+ public void xmppUriJoin() {
+ final var miniUri = new MiniUri("xmpp:room@chat.example.com?join");
+ Assert.assertEquals("xmpp", miniUri.getScheme());
+ Assert.assertNull(miniUri.getAuthority());
+ Assert.assertEquals("room@chat.example.com", miniUri.getPath());
+ Assert.assertEquals(ImmutableMap.of("join", ""), miniUri.getParameter());
+ }
+
+ @Test
+ public void xmppUriMessage() {
+ final var miniUri =
+ new MiniUri("xmpp:romeo@montague.net?message;body=Here%27s%20a%20test%20message");
+ Assert.assertEquals("xmpp", miniUri.getScheme());
+ Assert.assertNull(miniUri.getAuthority());
+ Assert.assertEquals("romeo@montague.net", miniUri.getPath());
+ Assert.assertEquals(
+ ImmutableMap.of("message", "", "body", "Here's a test message"),
+ miniUri.getParameter());
+ }
+}
diff --git a/src/test/java/de/gultsch/common/PatternTest.java b/src/test/java/de/gultsch/common/PatternTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..db18fc6c98322e530a0bd71a003389d5d2f36077
--- /dev/null
+++ b/src/test/java/de/gultsch/common/PatternTest.java
@@ -0,0 +1,120 @@
+package de.gultsch.common;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.regex.MatchResult;
+import java.util.stream.Collectors;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PatternTest {
+
+ @Test
+ public void shortImMessage() {
+ final var message =
+ "Hi. I'm refactoring how URIs are linked in Conversations. We now support more URI"
+ + " schemes like mailto:user@example.com and tel:+1-269-555-0107 and obviously"
+ + " maintain support for things like"
+ + " xmpp:conversations@conference.siacs.eu?join and https://example.com however"
+ + " we no longer link domains that aren't actual URIs like example.com to avoid"
+ + " some false positives.";
+
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(
+ Arrays.asList(
+ "mailto:user@example.com",
+ "tel:+1-269-555-0107",
+ "xmpp:conversations@conference.siacs.eu?join",
+ "https://example.com"),
+ matches);
+ }
+
+ @Test
+ public void ambiguous() {
+ final var message =
+ "Please find more information in the corresponding page on Wikipedia"
+ + " (https://en.wikipedia.org/wiki/Ambiguity_(disambiguation)). Let me know if"
+ + " you have questions!";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(
+ ImmutableList.of("https://en.wikipedia.org/wiki/Ambiguity_(disambiguation)"),
+ matches);
+ }
+
+ @Test
+ public void parenthesis() {
+ final var message = "Daniel is on Mastodon (https://gultsch.social/@daniel)";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("https://gultsch.social/@daniel"), matches);
+ }
+
+ @Test
+ public void fullWidthSpace() {
+ final var message = "\u3000https://conversations.im";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches);
+ }
+
+ @Test
+ public void fullWidthColon() {
+ final var message = "\uFF1Ahttps://conversations.im";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches);
+ }
+
+ @Test
+ public void newLine() {
+ final var message = "\nxmpp:example.com";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("xmpp:example.com"), matches);
+ }
+
+ @Test
+ public void code() {
+ final var message = "`xmpp:example.com`";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertTrue(matches.isEmpty());
+ }
+}