Merge branch 'master' of https://codeberg.org/iNPUTmice/Conversations
Some checks are pending
Android CI / build (push) Waiting to run

This commit is contained in:
Arne 2025-01-29 21:12:55 +01:00
parent fbb898e5aa
commit 3bd67c6d6d
72 changed files with 8105 additions and 4538 deletions

View file

@ -538,5 +538,12 @@
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0474.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
</Project>
</rdf:RDF>

View file

@ -124,14 +124,6 @@
android:name=".services.ImportBackupService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".services.ContactChooserTargetService"
android:exported="true"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>
<service
android:name=".services.CallIntegrationConnectionService"
@ -193,7 +185,9 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".ui.ConversationsActivity"
@ -364,10 +358,9 @@
<data android:mimeType="*/*" />
</intent-filter>
<!-- the value here needs to be the full class name; independent of the configured applicationId -->
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="eu.siacs.conversations.services.ContactChooserTargetService" />
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>
<activity
android:name=".ui.TrustKeysActivity"
@ -392,8 +385,7 @@
<activity
android:name=".ui.MediaBrowserActivity"
android:label="@string/media_browser"
android:exported="false" />
android:label="@string/media_browser" />
<activity android:name=".ui.AddReactionActivity" />
<activity

View file

@ -3,13 +3,10 @@ package eu.siacs.conversations;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.google.common.base.Strings;
import java.security.SecureRandom;
public class AppSettings {
@ -49,6 +46,8 @@ public class AppSettings {
public static final String SHOW_LINK_PREVIEWS = "show_link_previews";
public static final String SHOW_AVATARS = "show_avatars";
public static final String CALL_INTEGRATION = "call_integration";
public static final String ALIGN_START = "align_start";
private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
private static final String INSTALLATION_ID = "im.conversations.android.install_id";
public static final String SECURE_TLS = "secure_tls";
@ -128,6 +127,14 @@ public class AppSettings {
return getBooleanPreference(SHOW_AVATARS, R.bool.show_avatars);
}
public boolean isCallIntegration() {
return getBooleanPreference(CALL_INTEGRATION, R.bool.call_integration);
}
public boolean isAlignStart() {
return getBooleanPreference(ALIGN_START, R.bool.align_start);
}
public boolean isSecureTLS() {
return getBooleanPreference(SECURE_TLS, R.bool.secure_tls);
}
@ -136,10 +143,6 @@ public class AppSettings {
return getBooleanPreference(PREFER_IPV6, R.bool.prefer_ipv6);
}
public boolean isCallIntegration() {
return getBooleanPreference(CALL_INTEGRATION, R.bool.call_integration);
}
public boolean isUseTor() {
return getBooleanPreference(USE_TOR, R.bool.use_tor);
}

View file

@ -2,11 +2,9 @@ package eu.siacs.conversations;
import android.graphics.Bitmap;
import android.net.Uri;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -87,7 +85,6 @@ public final class Config {
public static final int CONNECT_DISCO_TIMEOUT = 20;
public static final int MINI_GRACE_PERIOD = 750;
// media file formats. Homogenous Android or Conversations only deployments can switch to opus
// and webp
public static final int AVATAR_SIZE = 480;

View file

@ -1,20 +1,15 @@
package eu.siacs.conversations.crypto.sasl;
import android.util.Log;
import com.google.common.base.CaseFormat;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.BiMap;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableBiMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.SSLSockets;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -35,20 +30,13 @@ public enum ChannelBinding {
SHORT_NAMES = builder.build();
}
public static Collection<ChannelBinding> of(final Element channelBinding) {
Preconditions.checkArgument(
channelBinding == null
|| ("sasl-channel-binding".equals(channelBinding.getName())
&& Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())),
"pass null or a valid channel binding stream feature");
public static Collection<ChannelBinding> of(final SaslChannelBinding channelBinding) {
if (channelBinding == null) {
return Collections.emptyList();
}
return Collections2.filter(
Collections2.transform(
Collections2.filter(
channelBinding == null
? Collections.emptyList()
: channelBinding.getChildren(),
c -> c != null && "channel-binding".equals(c.getName())),
c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))),
channelBinding.getChannelBindings(), cb -> ChannelBinding.of(cb.getType())),
Predicates.notNull());
}

View file

@ -0,0 +1,98 @@
package eu.siacs.conversations.crypto.sasl;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import java.util.Collection;
public class DowngradeProtection {
private static final char SEPARATOR = ',';
private static final char SEPARATOR_MECHANISM_AND_BINDING = '|';
public final ImmutableList<String> mechanisms;
public final ImmutableList<String> channelBindings;
public DowngradeProtection(
final Collection<String> mechanisms, final Collection<String> channelBindings) {
this.mechanisms = Ordering.natural().immutableSortedCopy(mechanisms);
this.channelBindings = Ordering.natural().immutableSortedCopy(channelBindings);
}
public DowngradeProtection(final Collection<String> mechanisms) {
this.mechanisms = Ordering.natural().immutableSortedCopy(mechanisms);
this.channelBindings = null;
}
public String asDString() {
ensureSaslMechanismFormat(this.mechanisms);
ensureNoSeparators(this.mechanisms);
if (this.channelBindings != null) {
ensureNoSeparators(this.channelBindings);
ensureBindingFormat(this.channelBindings);
final var builder = new StringBuilder();
Joiner.on(SEPARATOR).appendTo(builder, mechanisms);
builder.append(SEPARATOR_MECHANISM_AND_BINDING);
Joiner.on(SEPARATOR).appendTo(builder, channelBindings);
return builder.toString();
} else {
return Joiner.on(SEPARATOR).join(mechanisms);
}
}
private static void ensureNoSeparators(final Iterable<String> list) {
for (final String item : list) {
if (item.indexOf(SEPARATOR) >= 0
|| item.indexOf(SEPARATOR_MECHANISM_AND_BINDING) >= 0) {
throw new SecurityException("illegal chars found in list");
}
}
}
private static void ensureSaslMechanismFormat(final Iterable<String> names) {
for (final String name : names) {
ensureSaslMechanismFormat(name);
}
}
private static void ensureSaslMechanismFormat(final String name) {
if (Strings.isNullOrEmpty(name)) {
throw new SecurityException("Empty sasl mechanism names are not permitted");
}
// https://www.rfc-editor.org/rfc/rfc4422.html#section-3.1
if (name.length() <= 20
&& CharMatcher.inRange('A', 'Z')
.or(CharMatcher.inRange('0', '9'))
.or(CharMatcher.is('-'))
.or(CharMatcher.is('_'))
.matchesAllOf(name)
&& !Character.isDigit(name.charAt(0))) {
return;
}
throw new SecurityException("Encountered illegal sasl name");
}
private static void ensureBindingFormat(final Iterable<String> names) {
for (final String name : names) {
ensureBindingFormat(name);
}
}
private static void ensureBindingFormat(final String name) {
if (Strings.isNullOrEmpty(name)) {
throw new SecurityException("Empty binding names are not permitted");
}
// https://www.rfc-editor.org/rfc/rfc5056.html#section-7d
if (CharMatcher.inRange('A', 'Z')
.or(CharMatcher.inRange('a', 'z'))
.or(CharMatcher.inRange('0', '9'))
.or(CharMatcher.is('.'))
.or(CharMatcher.is('-'))
.matchesAllOf(name)) {
return;
}
throw new SecurityException("Encountered illegal binding name");
}
}

View file

@ -1,20 +1,15 @@
package eu.siacs.conversations.crypto.sasl;
import android.util.Log;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.SSLSockets;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import java.util.Collection;
import java.util.Collections;
import javax.net.ssl.SSLSocket;
public abstract class SaslMechanism {
@ -53,18 +48,7 @@ public abstract class SaslMechanism {
return "";
}
public static Collection<String> mechanisms(final Element authElement) {
if (authElement == null) {
return Collections.emptyList();
}
return Collections2.transform(
Collections2.filter(
authElement.getChildren(),
c -> c != null && "mechanism".equals(c.getName())),
c -> c == null ? null : c.getContent());
}
protected enum State {
public enum State {
INITIAL,
AUTH_TEXT_SENT,
RESPONSE_SENT,
@ -76,14 +60,11 @@ public abstract class SaslMechanism {
SASL_2;
public static Version of(final Element element) {
switch (Strings.nullToEmpty(element.getNamespace())) {
case Namespace.SASL:
return SASL;
case Namespace.SASL_2:
return SASL_2;
default:
throw new IllegalArgumentException("Unrecognized SASL namespace");
}
return switch (Strings.nullToEmpty(element.getNamespace())) {
case Namespace.SASL -> SASL;
case Namespace.SASL_2 -> SASL_2;
default -> throw new IllegalArgumentException("Unrecognized SASL namespace");
};
}
}

View file

@ -1,24 +1,27 @@
package eu.siacs.conversations.crypto.sasl;
import android.util.Base64;
import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
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.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.HashFunction;
import java.nio.charset.Charset;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
import java.security.InvalidKeyException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.crypto.SecretKey;
import javax.net.ssl.SSLSocket;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
abstract class ScramMechanism extends SaslMechanism {
public abstract class ScramMechanism extends SaslMechanism {
public static final SecretKey EMPTY_KEY =
new SecretKey() {
@ -46,8 +49,9 @@ abstract class ScramMechanism extends SaslMechanism {
private final String gs2Header;
private final String clientNonce;
protected State state = State.INITIAL;
private String clientFirstMessageBare;
private final String clientFirstMessageBare;
private byte[] serverSignature = null;
private DowngradeProtection downgradeProtection = null;
ScramMechanism(final Account account, final ChannelBinding channelBinding) {
super(account);
@ -67,28 +71,41 @@ abstract class ScramMechanism extends SaslMechanism {
}
// This nonce should be different for each authentication attempt.
this.clientNonce = CryptoHelper.random(100);
clientFirstMessageBare = "";
this.clientFirstMessageBare =
String.format(
"n=%s,r=%s",
CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())),
this.clientNonce);
}
public void setDowngradeProtection(final DowngradeProtection downgradeProtection) {
Preconditions.checkState(
this.state == State.INITIAL, "setting downgrade protection in invalid state");
this.downgradeProtection = downgradeProtection;
}
protected abstract HashFunction getHMac(final byte[] key);
protected abstract HashFunction getDigest();
private KeyPair getKeyPair(final String password, final String salt, final int iterations)
private KeyPair getKeyPair(final String password, final byte[] salt, final int iterations)
throws ExecutionException {
return CACHE.get(
new CacheKey(getMechanism(), password, salt, iterations),
() -> {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword =
hi(
password.getBytes(),
Base64.decode(salt, Base64.DEFAULT),
iterations);
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
});
final var key = new CacheKey(getMechanism(), password, salt, iterations);
return CACHE.get(key, () -> calculateKeyPair(password, salt, iterations));
}
private KeyPair calculateKeyPair(final String password, final byte[] salt, final int iterations)
throws InvalidKeyException {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(password.getBytes(), salt, iterations);
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
}
@Override
public String getMechanism() {
return "";
}
private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
@ -119,152 +136,167 @@ abstract class ScramMechanism extends SaslMechanism {
@Override
public String getClientFirstMessage(final SSLSocket sslSocket) {
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
clientFirstMessageBare =
"n="
+ CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername()))
+ ",r="
+ this.clientNonce;
state = State.AUTH_TEXT_SENT;
}
return Base64.encodeToString(
(gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
Preconditions.checkState(
this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state");
this.state = State.AUTH_TEXT_SENT;
final byte[] message = (gs2Header + clientFirstMessageBare).getBytes();
return BaseEncoding.base64().encode(message);
}
@Override
public String getResponse(final String challenge, final SSLSocket socket)
throws AuthenticationException {
switch (state) {
case AUTH_TEXT_SENT:
if (challenge == null) {
throw new AuthenticationException("challenge can not be null");
}
byte[] serverFirstMessage;
try {
serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
throw new AuthenticationException("Unable to decode server challenge", e);
}
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
String nonce = "";
int iterationCount = -1;
String salt = "";
for (final String token : tokenizer) {
if (token.length() > 1 && token.charAt(1) == '=') {
switch (token.charAt(0)) {
case 'i':
try {
iterationCount = Integer.parseInt(token.substring(2));
} catch (final NumberFormatException e) {
throw new AuthenticationException(e);
}
break;
case 's':
salt = token.substring(2);
break;
case 'r':
nonce = token.substring(2);
break;
case 'm':
/*
* RFC 5802:
* m: This attribute is reserved for future extensibility. In this
* version of SCRAM, its presence in a client or a server message
* MUST cause authentication failure when the attribute is parsed by
* the other end.
*/
throw new AuthenticationException(
"Server sent reserved token: `m'");
}
}
}
return switch (state) {
case AUTH_TEXT_SENT -> processServerFirstMessage(challenge, socket);
case RESPONSE_SENT -> processServerFinalMessage(challenge);
default -> throw new InvalidStateException(state);
};
}
if (iterationCount < 0) {
throw new AuthenticationException("Server did not send iteration count");
}
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
throw new AuthenticationException(
"Server nonce does not contain client nonce: " + nonce);
}
if (salt.isEmpty()) {
throw new AuthenticationException("Server sent empty salt");
}
final byte[] channelBindingData = getChannelBindingData(socket);
final int gs2Len = this.gs2Header.getBytes().length;
final byte[] cMessage = new byte[gs2Len + channelBindingData.length];
System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len);
System.arraycopy(
channelBindingData, 0, cMessage, gs2Len, channelBindingData.length);
final String clientFinalMessageWithoutProof =
"c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce;
final byte[] authMessage =
(clientFirstMessageBare
+ ','
+ new String(serverFirstMessage)
+ ','
+ clientFinalMessageWithoutProof)
.getBytes();
final KeyPair keys;
try {
keys =
getKeyPair(
CryptoHelper.saslPrep(account.getPassword()),
salt,
iterationCount);
} catch (ExecutionException e) {
throw new AuthenticationException("Invalid keys generated");
}
final byte[] clientSignature;
try {
serverSignature = hmac(keys.serverKey, authMessage);
final byte[] storedKey = digest(keys.clientKey);
clientSignature = hmac(storedKey, authMessage);
} catch (final InvalidKeyException e) {
throw new AuthenticationException(e);
}
final byte[] clientProof = new byte[keys.clientKey.length];
if (clientSignature.length < keys.clientKey.length) {
throw new AuthenticationException(
"client signature was shorter than clientKey");
}
for (int i = 0; i < clientProof.length; i++) {
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
}
final String clientFinalMessage =
clientFinalMessageWithoutProof
+ ",p="
+ Base64.encodeToString(clientProof, Base64.NO_WRAP);
state = State.RESPONSE_SENT;
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
case RESPONSE_SENT:
try {
final String clientCalculatedServerFinalMessage =
"v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP);
if (!clientCalculatedServerFinalMessage.equals(
new String(Base64.decode(challenge, Base64.DEFAULT)))) {
throw new Exception();
}
state = State.VALID_SERVER_RESPONSE;
return "";
} catch (Exception e) {
throw new AuthenticationException(
"Server final message does not match calculated final message");
}
default:
throw new InvalidStateException(state);
private String processServerFirstMessage(final String challenge, final SSLSocket socket)
throws AuthenticationException {
if (Strings.isNullOrEmpty(challenge)) {
throw new AuthenticationException("challenge can not be null");
}
byte[] serverFirstMessage;
try {
serverFirstMessage = BaseEncoding.base64().decode(challenge);
} catch (final IllegalArgumentException e) {
throw new AuthenticationException("Unable to decode server challenge", e);
}
final Map<String, String> attributes;
try {
attributes = splitToAttributes(new String(serverFirstMessage));
} catch (final IllegalArgumentException e) {
throw new AuthenticationException("Duplicate attributes");
}
if (attributes.containsKey("m")) {
/*
* RFC 5802:
* m: This attribute is reserved for future extensibility. In this
* version of SCRAM, its presence in a client or a server message
* MUST cause authentication failure when the attribute is parsed by
* the other end.
*/
throw new AuthenticationException("Server sent reserved token: 'm'");
}
final String i = attributes.get("i");
final String s = attributes.get("s");
final String nonce = attributes.get("r");
final String d = attributes.get("d");
if (Strings.isNullOrEmpty(s) || Strings.isNullOrEmpty(nonce) || Strings.isNullOrEmpty(i)) {
throw new AuthenticationException("Missing attributes from server first message");
}
final Integer iterationCount = Ints.tryParse(i);
if (iterationCount == null || iterationCount < 0) {
throw new AuthenticationException("Server did not send iteration count");
}
if (!nonce.startsWith(clientNonce)) {
throw new AuthenticationException(
"Server nonce does not contain client nonce: " + nonce);
}
final byte[] salt;
try {
salt = BaseEncoding.base64().decode(s);
} catch (final IllegalArgumentException e) {
throw new AuthenticationException("Invalid salt in server first message");
}
if (d != null && this.downgradeProtection != null) {
final String asSeenInFeatures;
try {
asSeenInFeatures = downgradeProtection.asDString();
} catch (final SecurityException e) {
throw new AuthenticationException(e);
}
final var hashed = BaseEncoding.base64().encode(digest(asSeenInFeatures.getBytes()));
if (!hashed.equals(d)) {
throw new AuthenticationException("Mismatch in SSDP");
}
}
final byte[] channelBindingData = getChannelBindingData(socket);
final int gs2Len = this.gs2Header.getBytes().length;
final byte[] cMessage = new byte[gs2Len + channelBindingData.length];
System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len);
System.arraycopy(channelBindingData, 0, cMessage, gs2Len, channelBindingData.length);
final String clientFinalMessageWithoutProof =
String.format("c=%s,r=%s", BaseEncoding.base64().encode(cMessage), nonce);
final var authMessage =
Joiner.on(',')
.join(
clientFirstMessageBare,
new String(serverFirstMessage),
clientFinalMessageWithoutProof);
final KeyPair keys;
try {
keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount);
} catch (final ExecutionException e) {
throw new AuthenticationException("Invalid keys generated");
}
final byte[] clientSignature;
try {
serverSignature = hmac(keys.serverKey, authMessage.getBytes());
final byte[] storedKey = digest(keys.clientKey);
clientSignature = hmac(storedKey, authMessage.getBytes());
} catch (final InvalidKeyException e) {
throw new AuthenticationException(e);
}
final byte[] clientProof = new byte[keys.clientKey.length];
if (clientSignature.length < keys.clientKey.length) {
throw new AuthenticationException("client signature was shorter than clientKey");
}
for (int j = 0; j < clientProof.length; j++) {
clientProof[j] = (byte) (keys.clientKey[j] ^ clientSignature[j]);
}
final var clientFinalMessage =
String.format(
"%s,p=%s",
clientFinalMessageWithoutProof, BaseEncoding.base64().encode(clientProof));
this.state = State.RESPONSE_SENT;
return BaseEncoding.base64().encode(clientFinalMessage.getBytes());
}
private Map<String, String> splitToAttributes(final String message) {
final ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
for (final String token : Splitter.on(',').split(message)) {
final var tuple = Splitter.on('=').limit(2).splitToList(token);
if (tuple.size() == 2) {
builder.put(tuple.get(0), tuple.get(1));
}
}
return builder.buildOrThrow();
}
private String processServerFinalMessage(final String challenge)
throws AuthenticationException {
final String serverFinalMessage;
try {
serverFinalMessage = new String(BaseEncoding.base64().decode(challenge));
} catch (final IllegalArgumentException e) {
throw new AuthenticationException("Invalid base64 in server final message", e);
}
final var clientCalculatedServerFinalMessage =
String.format("v=%s", BaseEncoding.base64().encode(serverSignature));
if (clientCalculatedServerFinalMessage.equals(serverFinalMessage)) {
this.state = State.VALID_SERVER_RESPONSE;
return "";
}
throw new AuthenticationException(
"Server final message does not match calculated final message");
}
protected byte[] getChannelBindingData(final SSLSocket sslSocket)
@ -276,12 +308,16 @@ abstract class ScramMechanism extends SaslMechanism {
}
private static class CacheKey {
final String algorithm;
final String password;
final String salt;
final int iterations;
private final String algorithm;
private final String password;
private final byte[] salt;
private final int iterations;
private CacheKey(String algorithm, String password, String salt, int iterations) {
private CacheKey(
final String algorithm,
final String password,
final byte[] salt,
final int iterations) {
this.algorithm = algorithm;
this.password = password;
this.salt = salt;
@ -289,19 +325,20 @@ abstract class ScramMechanism extends SaslMechanism {
}
@Override
public boolean equals(Object o) {
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return iterations == cacheKey.iterations
&& Objects.equal(algorithm, cacheKey.algorithm)
&& Objects.equal(password, cacheKey.password)
&& Objects.equal(salt, cacheKey.salt);
&& Arrays.equals(salt, cacheKey.salt);
}
@Override
public int hashCode() {
return Objects.hashCode(algorithm, password, salt, iterations);
final int result = Objects.hashCode(algorithm, password, iterations);
return 31 * result + Arrays.hashCode(salt);
}
}

View file

@ -7,6 +7,7 @@ import android.os.SystemClock;
import android.util.Log;
import androidx.core.graphics.ColorUtils;
import androidx.annotation.NonNull;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
@ -349,11 +350,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
this.password = password;
}
@NonNull
public String getHostname() {
return Strings.nullToEmpty(this.hostname);
}
public void setHostname(String hostname) {
public void setHostname(final String hostname) {
this.hostname = hostname;
}
@ -441,7 +443,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public HashedToken getFastMechanism() {
final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism);
final HashedToken.Mechanism fastMechanism =
HashedToken.Mechanism.ofOrNull(this.fastMechanism);
final String token = this.fastToken;
if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
return null;
@ -879,11 +882,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public enum State {
DISABLED(false, false),
LOGGED_OUT(false,false),
LOGGED_OUT(false, false),
OFFLINE(false),
CONNECTING(false),
ONLINE(false),
NO_INTERNET(false),
CONNECTION_TIMEOUT,
UNAUTHORIZED,
TEMPORARY_AUTH_FAILURE,
SERVER_NOT_FOUND,
@ -954,6 +958,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return R.string.account_status_not_found;
case NO_INTERNET:
return R.string.account_status_no_internet;
case CONNECTION_TIMEOUT:
return R.string.account_status_connection_timeout;
case REGISTRATION_FAILED:
return R.string.account_status_regis_fail;
case REGISTRATION_WEB:

View file

@ -32,7 +32,6 @@ import java.util.Objects;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.android.AbstractPhoneContact;
import eu.siacs.conversations.android.JabberIdContact;
import eu.siacs.conversations.persistance.FileBackend;
@ -686,7 +685,7 @@ public class Contact implements ListItem, Blockable {
public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
resetOption(getOption(clazz));
boolean changed = false;
if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
if (!getOption(Options.SYNCED_VIA_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
setSystemAccount(null);
changed |= setPhotoUri(null);
changed |= setSystemName(null);
@ -760,7 +759,7 @@ public class Contact implements ListItem, Blockable {
public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
if (clazz == JabberIdContact.class) {
return Options.SYNCED_VIA_ADDRESSBOOK;
return Options.SYNCED_VIA_ADDRESS_BOOK;
} else {
return Options.SYNCED_VIA_OTHER;
}
@ -803,7 +802,7 @@ public class Contact implements ListItem, Blockable {
public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
public static final int DIRTY_PUSH = 6;
public static final int DIRTY_DELETE = 7;
private static final int SYNCED_VIA_ADDRESSBOOK = 8;
private static final int SYNCED_VIA_ADDRESS_BOOK = 8;
public static final int SYNCED_VIA_OTHER = 9;
}
}

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.entities;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
@ -75,11 +77,12 @@ import com.google.android.material.color.MaterialColors;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.HashMultimap;
import io.ipfs.cid.Cid;
@ -158,6 +161,14 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Option;
import eu.siacs.conversations.xmpp.mam.MamReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
@ -169,7 +180,8 @@ import net.java.otr4j.session.SessionStatus;
import im.conversations.android.xmpp.model.stanza.Iq;
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
public class Conversation extends AbstractEntity
implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
public static final String TABLENAME = "conversations";
public static final int STATUS_AVAILABLE = 0;
@ -188,7 +200,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
public static final String ATTRIBUTE_NOTIFY_REPLIES = "notify_replies";
public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS =
"formerly_private_non_anonymous";
public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
@ -210,7 +223,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private int status;
private final long created;
private int mode;
private JSONObject attributes;
private final JSONObject attributes;
private Jid nextCounterpart;
private transient MucOptions mucOptions = null;
private boolean messagesLeftOnServer = true;
@ -236,17 +249,31 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
protected boolean anyMatchSpam = false;
public Conversation(final String name, final Account account, final Jid contactJid,
final int mode) {
this(java.util.UUID.randomUUID().toString(), name, null, account
.getUuid(), contactJid, System.currentTimeMillis(),
STATUS_AVAILABLE, mode, "");
public Conversation(
final String name, final Account account, final Jid contactJid, final int mode) {
this(
java.util.UUID.randomUUID().toString(),
name,
null,
account.getUuid(),
contactJid,
System.currentTimeMillis(),
STATUS_AVAILABLE,
mode,
"");
this.account = account;
}
public Conversation(final String uuid, final String name, final String contactUuid,
final String accountUuid, final Jid contactJid, final long created, final int status,
final int mode, final String attributes) {
public Conversation(
final String uuid,
final String name,
final String contactUuid,
final String accountUuid,
final Jid contactJid,
final long created,
final int status,
final int mode,
final String attributes) {
this.uuid = uuid;
this.name = name;
this.contactUuid = contactUuid;
@ -255,26 +282,37 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
this.created = created;
this.status = status;
this.mode = mode;
try {
this.attributes = new JSONObject(attributes == null ? "" : attributes);
} catch (JSONException e) {
this.attributes = new JSONObject();
this.attributes = parseAttributes(attributes);
}
private static JSONObject parseAttributes(final String attributes) {
if (Strings.isNullOrEmpty(attributes)) {
return new JSONObject();
} else {
try {
return new JSONObject(attributes);
} catch (final JSONException e) {
return new JSONObject();
}
}
}
public static Conversation fromCursor(Cursor cursor) {
return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
cursor.getString(cursor.getColumnIndex(NAME)),
cursor.getString(cursor.getColumnIndex(CONTACT)),
cursor.getString(cursor.getColumnIndex(ACCOUNT)),
JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
cursor.getLong(cursor.getColumnIndex(CREATED)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(MODE)),
cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
public static Conversation fromCursor(final Cursor cursor) {
return new Conversation(
cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTACT)),
cursor.getString(cursor.getColumnIndexOrThrow(ACCOUNT)),
JidHelper.parseOrFallbackToInvalid(
cursor.getString(cursor.getColumnIndexOrThrow(CONTACTJID))),
cursor.getLong(cursor.getColumnIndexOrThrow(CREATED)),
cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
cursor.getInt(cursor.getColumnIndexOrThrow(MODE)),
cursor.getString(cursor.getColumnIndexOrThrow(ATTRIBUTES)));
}
public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
public static Message getLatestMarkableMessage(
final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
for (int i = messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
if (message.getStatus() <= Message.STATUS_RECEIVED
@ -295,10 +333,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
final String contact = conversation.getJid().getDomain().toEscapedString();
final String account = conversation.getAccount().getServer();
if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact)
|| Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
return false;
}
return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
return conversation.isSingleOrPrivateAndNonAnonymous()
|| conversation.getBooleanAttribute(
ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
}
public boolean hasMessagesLeftOnServer() {
@ -350,7 +391,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public int countFailedDeliveries() {
int count = 0;
synchronized (this.messages) {
for(final Message message : this.messages) {
for (final Message message : this.messages) {
if (message.getStatus() == Message.STATUS_SEND_FAILED) {
++count;
}
@ -374,12 +415,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null;
}
public Message findUnsentMessageWithUuid(String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
final int s = message.getStatus();
if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING)
&& message.getUuid().equals(uuid)) {
return message;
}
}
@ -420,10 +461,16 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
synchronized (this.messages) {
for (final Message message : this.messages) {
final Transferable transferable = message.getTransferable();
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
final boolean unInitiatedButKnownSize =
MessageUtils.unInitiatedButKnownSize(message);
if (message.getUuid().equals(uuid)
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
&& (message.isFileOrImage()
|| message.treatAsDownloadable()
|| unInitiatedButKnownSize
|| (transferable != null
&& transferable.getStatus()
!= Transferable.STATUS_UPLOADING))) {
return message;
}
}
@ -450,7 +497,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (uuids.contains(message.getUuid())) {
message.setDeleted(true);
deleted = true;
if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
if (message.getEncryption() == Message.ENCRYPTION_PGP
&& pgpDecryptionService != null) {
pgpDecryptionService.discard(message);
}
}
@ -468,7 +516,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (file.uuid.toString().equals(message.getUuid())) {
message.setDeleted(file.deleted);
changed = true;
if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
if (file.deleted
&& message.getEncryption() == Message.ENCRYPTION_PGP
&& pgpDecryptionService != null) {
pgpDecryptionService.discard(message);
}
}
@ -496,7 +546,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public boolean setOutgoingChatState(ChatState state) {
if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
if (mode == MODE_SINGLE && !getContact().isSelf()
|| (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
if (this.mOutgoingChatState != state) {
this.mOutgoingChatState = state;
return true;
@ -540,7 +591,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
final ArrayList<Message> results = new ArrayList<>();
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost())
&& message.getStatus() == Message.STATUS_UNSEND) {
results.add(message);
}
}
@ -555,7 +607,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
for (Message message : this.messages) {
if (id.equals(message.getUuid())
|| (message.getStatus() >= Message.STATUS_SEND
&& id.equals(message.getRemoteMsgId()))) {
&& id.equals(message.getRemoteMsgId()))) {
return message;
}
}
@ -583,7 +635,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
continue;
}
if (counterpart == null || mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) {
final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
if (idMatch) return message;
}
}
@ -617,7 +669,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public Message findReceivedWithRemoteId(final String id) {
synchronized (this.messages) {
for (final Message message : this.messages) {
if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) {
if (message.getStatus() == Message.STATUS_RECEIVED
&& id.equals(message.getRemoteMsgId())) {
return message;
}
}
@ -896,7 +949,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public boolean setCorrectingMessage(Message correctingMessage) {
setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
setAttribute(
ATTRIBUTE_CORRECTING_MESSAGE,
correctingMessage == null ? null : correctingMessage.getUuid());
return correctingMessage == null && draftMessage != null;
}
@ -912,7 +967,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
@Override
public int compareTo(@NonNull Conversation another) {
return ComparisonChain.start()
.compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
.compareFalseFirst(
another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false),
getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
.compare(another.getSortableTime(), getSortableTime())
.result();
}
@ -1014,8 +1071,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return message;
}
public @NonNull
CharSequence getName() {
public @NonNull CharSequence getName() {
if (getMode() == MODE_MULTI) {
final String roomName = getMucOptions().getName();
final String subject = getMucOptions().getSubject();
@ -1035,6 +1091,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
}
}
} else if ((QuickConversationsService.isConversations()
|| !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain()))
&& isWithStranger()) {
return contactJid;
} else {
return this.getContact().getDisplayName();
}
@ -1224,9 +1284,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public int status = 0;
}
/**
* short for is Private and Non-anonymous
*/
/** short for is Private and Non-anonymous */
public boolean isSingleOrPrivateAndNonAnonymous() {
return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
}
@ -1263,7 +1321,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return Message.ENCRYPTION_NONE;
}
if (OmemoSetting.isAlways()) {
return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
return suitableForOmemoByDefault(this)
? Message.ENCRYPTION_AXOLOTL
: Message.ENCRYPTION_NONE;
}
final int defaultEncryption;
if (suitableForOmemoByDefault(this)) {
@ -1292,8 +1352,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return smp().status == Smp.STATUS_CONTACT_REQUESTED;
}
public @Nullable
Draft getDraft() {
public @Nullable Draft getDraft() {
long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
final long messageTime;
synchronized (this.messages) {
@ -1317,7 +1376,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
boolean changed = !getNextMessage().equals(message);
this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
if (changed) {
this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
this.setAttribute(
ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP,
message == null ? 0 : System.currentTimeMillis());
}
return changed;
}
@ -1353,7 +1414,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
Message message = this.messages.get(i);
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
if (message.getStatus() == Message.STATUS_UNSEND
|| message.getStatus() == Message.STATUS_SEND) {
String otherBody;
if (message.hasFileOnRemoteHost()) {
otherBody = message.getFileParams().url;
@ -1373,7 +1435,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = this.messages.get(i);
if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
if ((message.getStatus() == s)
&& (message.getType() == Message.TYPE_RTP_SESSION)
&& sessionId.equals(message.getRemoteMsgId())) {
return message;
}
}
@ -1387,7 +1451,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
synchronized (this.messages) {
for (Message message : this.messages) {
if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
if (serverMsgId.equals(message.getServerMsgId())
|| remoteMsgId.equals(message.getRemoteMsgId())) {
return true;
}
}
@ -1402,10 +1467,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = this.messages.get(i);
if (message.isPrivateMessage()) {
continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
continue; // it's unsafe to use private messages as anchor. They could be coming
// from user archive
}
if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
if (message.getStatus() == Message.STATUS_RECEIVED
|| message.isCarbon()
|| message.getServerMsgId() != null) {
lastReceived =
new MamReference(message.getTimeSent(), message.getServerMsgId());
break;
}
}
@ -1422,7 +1491,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public boolean alwaysNotify() {
return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
return mode == MODE_SINGLE
|| getBooleanAttribute(
ATTRIBUTE_ALWAYS_NOTIFY,
Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
}
public boolean notifyReplies() {
@ -1503,11 +1575,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
try {
list.add(Jid.of(array.getString(i)));
} catch (IllegalArgumentException e) {
//ignored
// ignored
}
}
} catch (JSONException e) {
//ignored
// ignored
}
}
return list;
@ -1605,7 +1677,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public void expireOldMessages(long timestamp) {
synchronized (this.messages) {
for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
for (ListIterator<Message> iterator = this.messages.listIterator();
iterator.hasNext(); ) {
if (iterator.next().getTimeSent() < timestamp) {
iterator.remove();
}
@ -1616,15 +1689,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public void sort() {
synchronized (this.messages) {
Collections.sort(this.messages, (left, right) -> {
if (left.getTimeSent() < right.getTimeSent()) {
return -1;
} else if (left.getTimeSent() > right.getTimeSent()) {
return 1;
} else {
return 0;
}
});
Collections.sort(
this.messages,
(left, right) -> {
if (left.getTimeSent() < right.getTimeSent()) {
return -1;
} else if (left.getTimeSent() > right.getTimeSent()) {
return 1;
} else {
return 0;
}
});
untieMessages();
}
}
@ -1638,8 +1713,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public int unreadCount(XmppConnectionService xmppConnectionService) {
synchronized (this.messages) {
int count = 0;
for (int i = messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
for (final Message message : Lists.reverse(this.messages)) {
if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue;
if (asReaction(message) != null) continue;
if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue;

View file

@ -195,7 +195,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public Message(Conversational conversation, String body, int encryption, int status) {
this(conversation, java.util.UUID.randomUUID().toString(),
this(
conversation,
java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
null,
@ -226,7 +228,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
this(conversation, java.util.UUID.randomUUID().toString(),
this(
conversation,
java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
null,
@ -256,13 +260,33 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null);
}
protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final boolean carbon,
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read,
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection<Reaction> reactions, final long timeReceived, final String subject, final String fileParams, final List<Element> payloads) {
protected Message(
final Conversational conversation,
final String uuid,
final String conversationUUid,
final Jid counterpart,
final Jid trueCounterpart,
final String body,
final long timeSent,
final int encryption,
final int status,
final int type,
final boolean carbon,
final String remoteMsgId,
final String relativeFilePath,
final String serverMsgId,
final String fingerprint,
final boolean read,
final String edited,
final boolean oob,
final String errorMessage,
final Set<ReadByMarker> readByMarkers,
final boolean markable,
final boolean deleted,
final String bodyLanguage,
final String occupantId,
final Collection<Reaction> reactions,
final long timeReceived, final String subject, final String fileParams, final List<Element> payloads) {
this.conversation = conversation;
this.uuid = uuid;
this.conversationUuid = conversationUUid;
@ -387,7 +411,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
} else {
values.put(TRUE_COUNTERPART, trueCounterpart.toString());
}
values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
values.put(
BODY,
body.length() > Config.MAX_STORAGE_MESSAGE_CHARS
? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS)
: body);
values.put(TIME_SENT, timeSent);
values.put(ENCRYPTION, encryption);
values.put(STATUS, status);
@ -543,7 +571,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
if (this.trueCounterpart == null) {
return null;
} else {
return this.conversation.getAccount().getRoster()
return this.conversation
.getAccount()
.getRoster()
.getContactFromContactList(this.trueCounterpart);
}
}
@ -721,8 +751,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public boolean setErrorMessage(String message) {
boolean changed = (message != null && !message.equals(errorMessage))
|| (message == null && errorMessage != null);
boolean changed =
(message != null && !message.equals(errorMessage))
|| (message == null && errorMessage != null);
this.errorMessage = message;
return changed;
}
@ -854,15 +885,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
boolean remoteMsgIdMatchInEdit(String id) {
for (Edit edit : this.edits) {
if (id.equals(edit.getEditedId())) {
return true;
}
}
return false;
}
public String getBodyLanguage() {
return this.bodyLanguage;
}
@ -872,7 +894,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public boolean edited() {
return this.edits.size() > 0;
return !this.edits.isEmpty();
}
public void setTrueCounterpart(Jid trueCounterpart) {
@ -906,7 +928,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
while (iterator.hasNext()) {
ReadByMarker marker = iterator.next();
if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
if (marker.getRealJid() == null
&& readByMarker.getFullJid().equals(marker.getFullJid())) {
iterator.remove();
}
}
@ -938,7 +961,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
boolean similar(Message message) {
if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
return this.serverMsgId.equals(message.getServerMsgId())
|| Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
} else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
return true;
} else if (this.body == null || this.counterpart == null) {
@ -954,13 +978,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
if (message.getRemoteMsgId() != null) {
final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
final boolean hasUuid =
CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
if (hasUuid
&& matchingCounterpart
&& Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
return true;
}
return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
return (message.getRemoteMsgId().equals(this.remoteMsgId)
|| message.getRemoteMsgId().equals(this.uuid))
&& matchingCounterpart
&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
&& (body.equals(otherBody)
|| (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
} else {
return this.remoteMsgId == null
&& matchingCounterpart
@ -971,15 +1000,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public Message next() {
if (this.conversation instanceof Conversation) {
final Conversation conversation = (Conversation) this.conversation;
synchronized (conversation.messages) {
if (this.conversation instanceof Conversation c) {
synchronized (c.messages) {
if (this.mNextMessage == null) {
int index = conversation.messages.indexOf(this);
if (index < 0 || index >= conversation.messages.size() - 1) {
int index = c.messages.indexOf(this);
if (index < 0 || index >= c.messages.size() - 1) {
this.mNextMessage = null;
} else {
this.mNextMessage = conversation.messages.get(index + 1);
this.mNextMessage = c.messages.get(index + 1);
}
}
return this.mNextMessage;
@ -990,15 +1018,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public Message prev() {
if (this.conversation instanceof Conversation) {
final Conversation conversation = (Conversation) this.conversation;
synchronized (conversation.messages) {
if (this.conversation instanceof Conversation c) {
synchronized (c.messages) {
if (this.mPreviousMessage == null) {
int index = conversation.messages.indexOf(this);
if (index <= 0 || index > conversation.messages.size()) {
int index = c.messages.indexOf(this);
if (index <= 0 || index > c.messages.size()) {
this.mPreviousMessage = null;
} else {
this.mPreviousMessage = conversation.messages.get(index - 1);
this.mPreviousMessage = c.messages.get(index - 1);
}
}
}
@ -1023,26 +1050,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
}
public boolean mergeable(final Message message) {
return false; // Merging messages messes up reply, so disable for now
}
private static boolean isStatusMergeable(int a, int b) {
return a == b || (
(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
);
}
private static boolean isEncryptionMergeable(final int a, final int b) {
return a == b
&& Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
.contains(a);
}
public void setCounterparts(List<MucOptions.User> counterparts) {
this.counterparts = counterparts;
}
@ -1053,7 +1060,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
@Override
public int getAvatarBackgroundColor() {
if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
if (type == Message.TYPE_STATUS
&& getCounterparts() != null
&& getCounterparts().size() > 1) {
return Color.TRANSPARENT;
} else {
return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
@ -1104,9 +1113,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.reactions = reactions;
}
public static class MergeSeparator {
}
public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
return getSpannableBody(thumbnailer, fallbackImg, true);
}
@ -1203,56 +1209,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return spannableBody;
}
public SpannableStringBuilder getMergedBody() {
return getMergedBody(null, null);
}
public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
Message current = this;
while (current.mergeable(current.next())) {
current = current.next();
if (current == null || current.getModerated() != null) {
break;
}
body.append("\n\n");
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
body.append(current.getSpannableBody(thumbnailer, fallbackImg));
}
return body;
public SpannableStringBuilder getSpannableBody() {
return getSpannableBody(null, null);
}
public boolean hasMeCommand() {
return this.body.trim().startsWith(ME_COMMAND);
}
public int getMergedStatus() {
int status = this.status;
Message current = this;
while (current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
status = current.status;
}
return status;
}
public long getMergedTimeSent() {
long time = this.timeSent;
Message current = this;
while (current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
time = current.timeSent;
}
return time;
}
public boolean wasMergedIntoPrevious(XmppConnectionService xmppConnectionService) {
Message prev = this.prev();
if (prev != null && getModerated() != null && prev.getModerated() != null) return true;
@ -1260,24 +1224,27 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
final boolean muted = getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), getOccupantId(), null, null));
if (prev != null && muted && getOccupantId().equals(prev.getOccupantId())) return true;
}
return prev != null && prev.mergeable(this);
return false;
}
public boolean trusted() {
Contact contact = this.getContact();
return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
final var contact = this.getContact();
return status > STATUS_RECEIVED
|| (contact != null && (contact.showInContactList() || contact.isSelf()));
}
public boolean fixCounterpart() {
final Presences presences = conversation.getContact().getPresences();
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
return true;
} else if (presences.size() >= 1) {
counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
return true;
} else {
} else if (presences.isEmpty()) {
counterpart = null;
return false;
} else {
counterpart =
PresenceSelector.getNextCounterpart(
getContact(), presences.toResourceArray()[0]);
return true;
}
}
@ -1286,19 +1253,17 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public String getEditedId() {
if (edits.size() > 0) {
return edits.get(edits.size() - 1).getEditedId();
} else {
throw new IllegalStateException("Attempting to store unedited message");
if (this.edits.isEmpty()) {
throw new IllegalStateException("Attempting to access unedited message");
}
return edits.get(edits.size() - 1).getEditedId();
}
public String getEditedIdWireFormat() {
if (edits.size() > 0) {
return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
} else {
throw new IllegalStateException("Attempting to store unedited message");
if (this.edits.isEmpty()) {
throw new IllegalStateException("Attempting to access unedited message");
}
return edits.get(0).getEditedId();
}
public List<URI> getLinks() {
@ -1530,7 +1495,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
}
public boolean isTypeText() {
return type == TYPE_TEXT || type == TYPE_PRIVATE;
}
@ -1814,7 +1778,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public boolean isTrusted() {
final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
final FingerprintStatus s =
axolotlService != null
? axolotlService.getFingerprintTrust(axolotlFingerprint)
: null;
return s != null && s.isTrusted();
}
@ -1829,17 +1796,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
private int getNextEncryption() {
if (this.conversation instanceof Conversation) {
Conversation conversation = (Conversation) this.conversation;
if (this.conversation instanceof Conversation c) {
for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
continue;
}
return iterator.getEncryption();
}
return conversation.getNextEncryption();
return c.getNextEncryption();
} else {
throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
throw new AssertionError(
"This should never be called since isInValidSession should be disabled for"
+ " stubs");
}
}
@ -1847,9 +1815,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
int futureEncryption = getCleanedEncryption(this.getNextEncryption());
boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
|| futureEncryption == ENCRYPTION_NONE
|| pastEncryption != futureEncryption;
boolean inUnencryptedSession =
pastEncryption == ENCRYPTION_NONE
|| futureEncryption == ENCRYPTION_NONE
|| pastEncryption != futureEncryption;
return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
}
@ -1858,7 +1827,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
return ENCRYPTION_PGP;
}
if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
|| encryption == ENCRYPTION_AXOLOTL_FAILED) {
return ENCRYPTION_AXOLOTL;
}
return encryption;
@ -1896,7 +1866,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return configurePrivateMessage(conversation, message, counterpart, false);
}
private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
private static boolean configurePrivateMessage(
final Conversation conversation,
final Message message,
final Jid counterpart,
final boolean isFile) {
if (counterpart == null) {
return false;
}

View file

@ -7,18 +7,12 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import io.ipfs.cid.Cid;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.services.AvatarService;
@ -35,6 +29,7 @@ import eu.siacs.conversations.xml.Element;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -66,7 +61,7 @@ public class MucOptions {
public MucOptions(final Conversation conversation) {
this.account = conversation.getAccount();
this.conversation = conversation;
final String nick = getProposedNickPure(conversation.getAttribute("mucNick"));
final String nick = getProposedNick(conversation.getAttribute("mucNick"));
this.self = new User(this, createJoinJid(nick), null, nick, new HashSet<>());
this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
this.self.role = Role.of(conversation.getAttribute("role"));
@ -105,10 +100,14 @@ public class MucOptions {
return mAutoPushConfiguration;
}
public boolean isSelf(Jid counterpart) {
public boolean isSelf(final Jid counterpart) {
return counterpart.equals(self.getFullJid());
}
public boolean isSelf(final String occupantId) {
return occupantId.equals(self.getOccupantId());
}
public void resetChatState() {
synchronized (users) {
for (User user : users) {
@ -350,29 +349,24 @@ public class MucOptions {
return null;
}
public User findUserByOccupantId(final String id, final Jid counterpart) {
if (id == null) {
return null;
public User findUserByOccupantId(final String occupantId, final Jid counterpart) {
synchronized (this.users) {
final var found = Strings.isNullOrEmpty(occupantId) ? null : Iterables.find(this.users, u -> occupantId.equals(u.occupantId),null);
if (Strings.isNullOrEmpty(occupantId) || found != null) return found;
final var user = new User(this, counterpart, occupantId, null, new HashSet<>());
user.setOnline(false);
return user;
}
synchronized (users) {
for (User user : users) {
if (id.equals(user.getOccupantId())) {
return user;
}
}
}
final var user = new User(this, counterpart, id, null, new HashSet<>());
user.setOnline(false);
return user;
}
public User findOrCreateUserByRealJid(Jid jid, Jid fullJid, final String occupantId) {
User user = findUserByRealJid(jid);
if (user == null) {
user = new User(this, fullJid, occupantId, null, new HashSet<>());
user.setRealJid(jid);
user.setOnline(false);
final User existing = findUserByRealJid(jid);
if (existing != null) {
return existing;
}
final var user = new User(this, fullJid, occupantId, null, new HashSet<>());
user.setRealJid(jid);
user.setOnline(false);
return user;
}
@ -386,6 +380,31 @@ public class MucOptions {
}
}
private User findUser(final Reaction reaction) {
if (reaction.trueJid != null) {
return findOrCreateUserByRealJid(reaction.trueJid.asBareJid(), reaction.from, reaction.occupantId);
}
final var existing = findUserByOccupantId(reaction.occupantId, reaction.from);
if (existing != null) {
return existing;
} else if (reaction.from != null) {
return new User(this,reaction.from,reaction.occupantId,null,new HashSet<>());
} else {
return null;
}
}
public List<User> findUsers(final Collection<Reaction> reactions) {
final ImmutableList.Builder<User> builder = new ImmutableList.Builder<>();
for(final Reaction reaction : reactions) {
final var user = findUser(reaction);
if (user != null) {
builder.add(user);
}
}
return builder.build();
}
public boolean isContactInRoom(Contact contact) {
return contact != null && isUserInRoom(findUserByRealJid(contact.getJid().asBareJid()));
}
@ -490,13 +509,19 @@ public class MucOptions {
}
private String getProposedNick() {
return getProposedNick(null);
}
private String getProposedNick(final String mucNick) {
final Bookmark bookmark = this.conversation.getBookmark();
if (bookmark != null) {
// if we already have a bookmark we consider this the source of truth
return getProposedNickPure();
}
final var storedJid = conversation.getJid();
if (storedJid.isBareJid()) {
if (mucNick != null) {
return mucNick;
} else if (storedJid.isBareJid()) {
return defaultNick(account);
} else {
return storedJid.getResource();
@ -504,16 +529,10 @@ public class MucOptions {
}
public String getProposedNickPure() {
return getProposedNickPure(null);
}
public String getProposedNickPure(final String mucNick) {
final Bookmark bookmark = this.conversation.getBookmark();
final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
if (bookmarkedNick != null) {
return bookmarkedNick;
} else if (mucNick != null) {
return mucNick;
} else {
return defaultNick(account);
}
@ -536,7 +555,7 @@ public class MucOptions {
try {
return account.withResource(nick).getResource();
} catch (final IllegalArgumentException e) {
return nick;
return null;
}
}
@ -586,9 +605,6 @@ public class MucOptions {
}
public boolean setSubject(String subject) {
if (!Objects.equals(getSubject(), subject)) {
this.conversation.setAttribute("subjectTs", String.valueOf(System.currentTimeMillis()));
}
return this.conversation.setAttribute("subject", subject);
}
@ -912,7 +928,7 @@ public class MucOptions {
private final MucOptions options;
private ChatState chatState = Config.DEFAULT_CHAT_STATE;
protected Set<Hat> hats;
private String occupantId;
protected String occupantId;
protected boolean online = true;
public User(MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
@ -949,7 +965,7 @@ public class MucOptions {
}
public String getOccupantId() {
return this.occupantId;
return occupantId;
}
public String getNick() {

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.entities;
import android.util.Log;
import androidx.annotation.NonNull;
import de.monocles.chat.EmojiSearch;
@ -12,17 +14,19 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
import com.google.gson.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import io.ipfs.cid.Cid;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.xmpp.Jid;
import java.io.IOException;
@ -77,6 +81,10 @@ public class Reaction {
this.envelopeId = envelopeId;
}
public String normalizedReaction() {
return Emoticons.normalizeToVS16(this.reaction);
}
public static String toString(final Collection<Reaction> reactions) {
return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions);
}
@ -88,6 +96,7 @@ public class Reaction {
try {
return GSON.fromJson(asString, new TypeToken<List<Reaction>>() {}.getType());
} catch (final IllegalArgumentException | JsonSyntaxException e) {
Log.e(Config.LOGTAG, "could not restore reactions", e);
return Collections.emptyList();
}
}
@ -226,7 +235,7 @@ public class Reaction {
ImmutableSet.copyOf(
Collections2.transform(
Collections2.filter(reactions, r -> r.cid == null && !r.received),
r -> r.reaction)));
Reaction::normalizedReaction)));
}
public static final class Aggregated {

View file

@ -3,13 +3,6 @@ package eu.siacs.conversations.generator;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
@ -27,22 +20,35 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.reactions.Reaction;
import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.unique.OriginId;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
public class MessageGenerator extends AbstractGenerator {
public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesnt seem to support that";
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesnt seem to support that. Find more information on https://conversations.im/omemo";
private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesnt seem to support that.";
private static final String OMEMO_FALLBACK_MESSAGE =
"I sent you an OMEMO encrypted message but your client doesnt seem to support that."
+ " Find more information on https://conversations.im/omemo";
private static final String PGP_FALLBACK_MESSAGE =
"I sent you a PGP encrypted message but your client doesnt seem to support that.";
public MessageGenerator(XmppConnectionService service) {
super(service);
}
private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message, boolean legacyEncryption) {
private im.conversations.android.xmpp.model.stanza.Message preparePacket(
final Message message, final boolean legacyEncryption) {
Conversation conversation = (Conversation) message.getConversation();
Account account = conversation.getAccount();
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
final boolean isWithSelf = conversation.getContact().isSelf();
if (conversation.getMode() == Conversation.MODE_SINGLE) {
packet.setTo(message.getCounterpart());
@ -64,11 +70,13 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
if (conversation.getMode() == Conversational.MODE_SINGLE || message.isPrivateMessage() || !conversation.getMucOptions().stableId()) {
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid());
if (conversation.getMode() == Conversational.MODE_MULTI
&& !message.isPrivateMessage()
&& !conversation.getMucOptions().stableId()) {
packet.addExtension(new OriginId(message.getUuid()));
}
if (message.edited()) {
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
packet.addExtension(new Replace(message.getEditedIdWireFormat()));
}
if (!legacyEncryption) {
if (message.getSubject() != null && message.getSubject().length() > 0) packet.addChild("subject").setContent(message.getSubject());
@ -84,16 +92,18 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public void addDelay(im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
final SimpleDateFormat mDateFormat = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
public void addDelay(
im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
final SimpleDateFormat mDateFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Element delay = packet.addChild("delay", "urn:xmpp:delay");
Date date = new Date(timestamp);
delay.setAttribute("stamp", mDateFormat.format(date));
}
public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(
Message message, XmppAxolotlMessage axolotlMessage) {
im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
if (axolotlMessage == null) {
return null;
@ -107,8 +117,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(
Jid to, XmppAxolotlMessage axolotlMessage) {
im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(to);
packet.setAxolotlMessage(axolotlMessage.toElement());
@ -200,23 +212,32 @@ public class MessageGenerator extends AbstractGenerator {
}
}
public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) {
public im.conversations.android.xmpp.model.stanza.Message generateChatState(
Conversation conversation) {
final Account account = conversation.getAccount();
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(
conversation.getMode() == Conversation.MODE_MULTI
? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
: im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(conversation.getJid().asBareJid());
packet.setFrom(account.getJid());
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
packet.addChild("no-store", "urn:xmpp:hints");
packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
packet.addChild("no-storage", "urn:xmpp:hints"); // wrong! don't copy this. Its *store*
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) {
final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI;
final Jid to = message.getCounterpart();
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(
groupChat
? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
: im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(groupChat ? to.asBareJid() : to);
final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
if (groupChat) {
@ -234,15 +255,22 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message reaction(final Conversational conversation, final Message inReplyTo, final String reactingTo, final Collection<String> ourReactions) {
final boolean groupChat = conversation.getMode() == Conversational.MODE_MULTI;
final Jid to = conversation.getJid().asBareJid();
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
public im.conversations.android.xmpp.model.stanza.Message reaction(
final Jid to,
final boolean groupChat,
final Message inReplyTo,
final String reactingTo,
final Collection<String> ourReactions) {
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(
groupChat
? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
: im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(to);
final var reactions = packet.addExtension(new Reactions());
reactions.setId(reactingTo);
for(final String ourReaction : ourReactions) {
for (final String ourReaction : ourReactions) {
reactions.addExtension(new Reaction(ourReaction));
}
@ -253,8 +281,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(
Conversation conversation, String subject) {
im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
packet.setTo(conversation.getJid().asBareJid());
packet.addChild("subject").setContent(subject);
@ -274,8 +304,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message directInvite(final Conversation conversation, final Jid contact) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
public im.conversations.android.xmpp.model.stanza.Message directInvite(
final Conversation conversation, final Jid contact) {
im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
packet.setTo(contact);
packet.setFrom(conversation.getAccount().getJid());
@ -292,7 +324,8 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) {
public im.conversations.android.xmpp.model.stanza.Message invite(
final Conversation conversation, final Jid contact) {
final var packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setTo(conversation.getJid().asBareJid());
packet.setFrom(conversation.getAccount().getJid());
@ -305,9 +338,13 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message received(Account account, final Jid from, final String id, ArrayList<String> namespaces, im.conversations.android.xmpp.model.stanza.Message.Type type) {
final var receivedPacket =
new im.conversations.android.xmpp.model.stanza.Message();
public im.conversations.android.xmpp.model.stanza.Message received(
Account account,
final Jid from,
final String id,
ArrayList<String> namespaces,
im.conversations.android.xmpp.model.stanza.Message.Type type) {
final var receivedPacket = new im.conversations.android.xmpp.model.stanza.Message();
receivedPacket.setType(type);
receivedPacket.setTo(from);
receivedPacket.setFrom(account.getJid());
@ -318,8 +355,10 @@ public class MessageGenerator extends AbstractGenerator {
return receivedPacket;
}
public im.conversations.android.xmpp.model.stanza.Message received(Account account, Jid to, String id) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
public im.conversations.android.xmpp.model.stanza.Message received(
Account account, Jid to, String id) {
im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setFrom(account.getJid());
packet.setTo(to);
packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
@ -329,7 +368,8 @@ public class MessageGenerator extends AbstractGenerator {
public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
final Jid with, final String sessionId, final Reason reason) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(with);
final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
@ -353,24 +393,33 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
public im.conversations.android.xmpp.model.stanza.Message sessionProposal(
final JingleConnectionManager.RtpSessionProposal proposal) {
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(
im.conversations.android.xmpp.model.stanza.Message.Type
.CHAT); // we want to carbon copy those
packet.setTo(proposal.with);
packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
for (final Media media : proposal.media) {
propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString());
propose.addChild("description", Namespace.JINGLE_APPS_RTP)
.setAttribute("media", media.toString());
}
packet.addChild("request", "urn:xmpp:receipts");
packet.addChild("store", "urn:xmpp:hints");
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
public im.conversations.android.xmpp.model.stanza.Message sessionRetract(
final JingleConnectionManager.RtpSessionProposal proposal) {
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(
im.conversations.android.xmpp.model.stanza.Message.Type
.CHAT); // we want to carbon copy those
packet.setTo(proposal.with);
final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
@ -379,9 +428,13 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public im.conversations.android.xmpp.model.stanza.Message sessionReject(final Jid with, final String sessionId) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
public im.conversations.android.xmpp.model.stanza.Message sessionReject(
final Jid with, final String sessionId) {
final im.conversations.android.xmpp.model.stanza.Message packet =
new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(
im.conversations.android.xmpp.model.stanza.Message.Type
.CHAT); // we want to carbon copy those
packet.setTo(with);
final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", sessionId);

View file

@ -9,6 +9,7 @@ import android.util.Pair;
import de.monocles.chat.BobTransfer;
import de.monocles.chat.WebxdcUpdate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import net.java.otr4j.session.Session;
@ -76,6 +77,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.carbons.Received;
import im.conversations.android.xmpp.model.carbons.Sent;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.forward.Forwarded;
import im.conversations.android.xmpp.model.occupant.OccupantId;
import im.conversations.android.xmpp.model.reactions.Reactions;
@ -579,7 +581,9 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
}
final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
final boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
Set<Message.FileParams> attachments = new LinkedHashSet<>();
for (Element child : packet.getChildren()) {
@ -614,6 +618,7 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
LocalizedContent body = packet.getBody();
final var reactions = packet.getExtension(Reactions.class);
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
@ -637,12 +642,36 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'");
return;
}
boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
if (query != null && !query.muc() && isTypeGroupChat) {
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
return;
}
final Jid mucTrueCounterPart;
final OccupantId occupant;
if (isTypeGroupChat) {
final Conversation conversation =
mXmppConnectionService.find(account, from.asBareJid());
final Jid mucTrueCounterPartByPresence;
if (conversation != null) {
final var mucOptions = conversation.getMucOptions();
occupant = mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
final var user =
occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId(), from);
mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
} else {
occupant = null;
mucTrueCounterPartByPresence = null;
}
mucTrueCounterPart =
getTrueCounterpart(
(query != null && query.safeToExtractTrueCounterpart())
? mucUserElement
: null,
mucTrueCounterPartByPresence);
} else {
mucTrueCounterPart = null;
occupant = null;
}
boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() == 0);
boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
boolean selfAddressed;
@ -732,7 +761,14 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
if (selfAddressed) {
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) {
// dont store serverMsgId on reflections for edits
final var reflectedServerMsgId =
Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
if (mXmppConnectionService.markMessage(
conversation,
remoteMsgId,
Message.STATUS_SEND_RECEIVED,
reflectedServerMsgId)) {
return;
}
status = Message.STATUS_RECEIVED;
@ -745,7 +781,10 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
if (conversation.getMucOptions().isSelf(counterpart)) {
status = Message.STATUS_SEND_RECEIVED;
isCarbon = true; //not really carbon but received from another resource
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId, body, html, packet.findChildContent("subject"), packet.findChild("thread"), attachments)) {
// dont store serverMsgId on reflections for edits
final var reflectedServerMsgId =
Strings.isNullOrEmpty(replacementId) ? serverMsgId : null;
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, reflectedServerMsgId, body, html, packet.findChildContent("subject"), packet.findChild("thread"), attachments)) {
return;
} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
if (body != null) {
@ -897,10 +936,8 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
}
if (conversationMultiMode) {
final var mucOptions = conversation.getMucOptions();
final var occupantId =
mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
if (occupantId != null) {
message.setOccupantId(occupantId.getId());
if (occupant != null) {
message.setOccupantId(occupant.getId());
}
message.setMucUser(mucOptions.findUserByFullJid(counterpart));
final Jid fallback = mucOptions.getTrueCounterpart(counterpart);
@ -931,6 +968,7 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, counterpart);
Log.d("WUT", "" + replacementId + " " + replacedMessage);
if (replacedMessage != null) {
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
@ -981,14 +1019,27 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
}
}
replacedMessage.setInReplyTo(message.getInReplyTo());
if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
replacedMessage.setServerMsgId(message.getServerMsgId());
}
// we store the IDs of the replacing message. This is essentially unused
// today (only the fact that there are _some_ edits causes the edit icon
// to appear)
replacedMessage.putEdited(
message.getRemoteMsgId(), message.getServerMsgId());
// we used to call
// `replacedMessage.setServerMsgId(message.getServerMsgId());` so during
// catchup we could start from the edit; not the original message
// however this caused problems for things like reactions that refer to
// the serverMsgId
replacedMessage.setEncryption(message.getEncryption());
if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
replacedMessage.markUnread();
}
extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet);
extractChatState(
mXmppConnectionService.find(account, counterpart.asBareJid()),
isTypeGroupChat,
packet);
mXmppConnectionService.updateMessage(replacedMessage, uuid);
if (mXmppConnectionService.confirmMessages()
&& replacedMessage.getStatus() == Message.STATUS_RECEIVED
@ -1235,8 +1286,7 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
// the 'ringing' response. Responding with delivery receipts predates
// the 'ringing' spec'd
final boolean sendReceipts =
(mXmppConnectionService.confirmMessages()
&& contact.showInContactList())
contact.showInContactList()
|| Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK;
if (remoteMsgId != null && !contact.isSelf() && sendReceipts) {
processMessageReceipts(account, packet, remoteMsgId, null);
@ -1430,12 +1480,9 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
if (conversation != null && reactingTo != null) {
if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
final var mucOptions = conversation.getMucOptions();
final var occupant =
mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
final var occupantId = occupant == null ? null : occupant.getId();
if (occupantId != null) {
// TODO use occupant id for isSelf assessment
final boolean isReceived = !mucOptions.isSelf(counterpart);
final boolean isReceived = !mucOptions.isSelf(occupantId);
final Message message;
final var inMemoryMessage =
conversation.findMessageWithServerMsgId(reactingTo);

View file

@ -33,7 +33,6 @@ import android.util.DisplayMetrics;
import android.util.Pair;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.FileProvider;
@ -170,7 +169,8 @@ public class FileBackend {
if (dimensions.getMin() > 720) {
Log.d(
Config.LOGTAG,
"do not consider video file with min width larger than 720 for size check");
"do not consider video file with min width larger than 720 for size"
+ " check");
continue;
}
} catch (final IOException | NotAVideoFile e) {
@ -300,7 +300,8 @@ public class FileBackend {
return inSampleSize;
}
private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile, IOException {
private static Dimensions getVideoDimensions(Context context, Uri uri)
throws NotAVideoFile, IOException {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
try {
mediaMetadataRetriever.setDataSource(context, uri);
@ -836,10 +837,10 @@ public class FileBackend {
return is;
}
public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
public void copyFileToPrivateStorage(final File file, final Uri uri) throws FileCopyException {
final var parentDirectory = file.getParentFile();
if (parentDirectory != null && parentDirectory.mkdirs()) {
Log.d(Config.LOGTAG,"created directory "+parentDirectory.getAbsolutePath());
Log.d(Config.LOGTAG, "created directory " + parentDirectory.getAbsolutePath());
}
try {
if (!file.createNewFile() && file.length() > 0) {
@ -879,9 +880,10 @@ public class FileBackend {
}
}
public void copyFileToPrivateStorage(Message message, Uri uri, String type)
public void copyFileToPrivateStorage(final Message message, final Uri uri, final String type)
throws FileCopyException {
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
final String mime =
MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")");
String extension = MimeUtils.guessExtensionFromMimeType(mime);
if (extension == null) {
@ -2047,7 +2049,7 @@ public class FileBackend {
return calcSampleSize(options, size);
}
public void updateFileParams(Message message) {
public void updateFileParams(final Message message) {
updateFileParams(message, null);
}

View file

@ -5,18 +5,9 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.NonNull;
import com.otaliastudios.transcoder.Transcoder;
import com.otaliastudios.transcoder.TranscoderListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
@ -26,6 +17,11 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.TranscoderStrategies;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class AttachFileToConversationRunnable implements Runnable, TranscoderListener {
@ -39,16 +35,26 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
private final long originalFileSize;
private int currentProgress = -1;
AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
AttachFileToConversationRunnable(
XmppConnectionService xmppConnectionService,
Uri uri,
String type,
Message message,
UiCallback<Message> callback) {
this.uri = uri;
this.type = type;
this.mXmppConnectionService = xmppConnectionService;
this.message = message;
this.callback = callback;
mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
mimeType =
MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
final int autoAcceptFileSize =
mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
this.isVideoMessage =
(mimeType != null && mimeType.startsWith("video/"))
&& originalFileSize > autoAcceptFileSize
&& !"uncompressed".equals(getVideoCompression());
}
boolean isVideoMessage() {
@ -75,7 +81,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
}
} else {
try {
mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
mXmppConnectionService
.getFileBackend()
.copyFileToPrivateStorage(message, uri, type);
mXmppConnectionService.getFileBackend().updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
@ -87,16 +95,26 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
} else {
mXmppConnectionService.sendMessage(message, () -> callback.success(message));
}
} catch (FileBackend.FileCopyException e) {
} catch (final FileBackend.FileCopyException e) {
callback.error(e.getResId(), message);
}
}
}
private void fallbackToProcessAsFile() {
final var file = mXmppConnectionService.getFileBackend().getFile(message);
if (file.exists() && file.delete()) {
Log.d(Config.LOGTAG, "deleted preexisting file " + file.getAbsolutePath());
}
XmppConnectionService.FILE_ATTACHMENT_EXECUTOR.execute(this::processAsFile);
}
private void processAsVideo() throws FileNotFoundException {
Log.d(Config.LOGTAG, "processing file as video");
mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification();
mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
mXmppConnectionService
.getFileBackend()
.setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
Log.d(Config.LOGTAG, "created parent directory for video file");
@ -106,16 +124,23 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
final Future<Void> future;
try {
future = Transcoder.into(file.getAbsolutePath()).
addDataSource(mXmppConnectionService, uri)
.setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
.setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
.setListener(this)
.transcode();
future =
Transcoder.into(file.getAbsolutePath())
.addDataSource(mXmppConnectionService, uri)
.setVideoTrackStrategy(
highQuality
? TranscoderStrategies.VIDEO_720P
: TranscoderStrategies.VIDEO_360P)
.setAudioTrackStrategy(
highQuality
? TranscoderStrategies.AUDIO_HQ
: TranscoderStrategies.AUDIO_MQ)
.setListener(this)
.transcode();
} catch (final RuntimeException e) {
// transcode can already throw if there is an invalid file format or a platform bug
mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
processAsFile();
fallbackToProcessAsFile();
return;
}
try {
@ -125,9 +150,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
} catch (final ExecutionException e) {
if (e.getCause() instanceof Error) {
mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
processAsFile();
fallbackToProcessAsFile();
} else {
Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
Log.d(Config.LOGTAG, "ignoring execution exception. Handled by onTranscodeFiled()");
}
}
}
@ -137,7 +162,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
final int p = (int) Math.round(progress * 100);
if (p > currentProgress) {
currentProgress = p;
mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message);
mXmppConnectionService
.getNotificationService()
.updateFileAddingNotification(p, message);
}
}
@ -146,11 +173,15 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
final File file = mXmppConnectionService.getFileBackend().getFile(message);
long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
Log.d(
Config.LOGTAG,
"originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
if (file.delete()) {
Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file");
processAsFile();
Log.d(
Config.LOGTAG,
"original file size was smaller. deleting and processing as file");
fallbackToProcessAsFile();
return;
} else {
Log.d(Config.LOGTAG, "unable to delete converted file");
@ -167,14 +198,14 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
@Override
public void onTranscodeCanceled() {
mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
processAsFile();
fallbackToProcessAsFile();
}
@Override
public void onTranscodeFailed(@NonNull final Throwable exception) {
mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
Log.d(Config.LOGTAG, "video transcoding failed", exception);
processAsFile();
fallbackToProcessAsFile();
}
@Override
@ -182,7 +213,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
if (this.isVideoMessage()) {
try {
processAsVideo();
} catch (FileNotFoundException e) {
} catch (final FileNotFoundException e) {
processAsFile();
}
} else {
@ -195,7 +226,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis
}
public static String getVideoCompression(final Context context) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
final SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getString(
"video_compression", context.getResources().getString(R.string.video_compression));
}
}

View file

@ -2,7 +2,6 @@ package eu.siacs.conversations.services;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.net.Uri;
@ -20,7 +19,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
@ -545,6 +543,10 @@ public class CallIntegration extends Connection {
if (Build.MODEL.endsWith("(AppSupport)")) {
return false;
}
// SailfishOS's AppSupport do not support Call Integration
if (Build.MODEL.endsWith("(AppSupport)")) {
return false;
}
return true;
}

View file

@ -47,7 +47,6 @@ import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
@ -55,7 +54,6 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@ -81,7 +79,6 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@ -95,6 +92,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -131,7 +129,7 @@ public class NotificationService {
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX =
"incoming_calls_channel#";
private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
public static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
NotificationService(final XmppConnectionService service) {
this.mXmppConnectionService = service;
@ -246,25 +244,8 @@ public class NotificationService {
missedCallsChannel.setGroup("calls");
notificationManager.createNotificationChannel(missedCallsChannel);
final NotificationChannel messagesChannel =
new NotificationChannel(
MESSAGES_NOTIFICATION_CHANNEL,
c.getString(R.string.messages_channel_name),
NotificationManager.IMPORTANCE_HIGH);
messagesChannel.setShowBadge(true);
messagesChannel.setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build());
messagesChannel.setLightColor(LED_COLOR);
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
messagesChannel.setVibrationPattern(pattern);
messagesChannel.enableVibration(true);
messagesChannel.enableLights(true);
messagesChannel.setGroup("chats");
final var messagesChannel =
prepareMessagesChannel(mXmppConnectionService, MESSAGES_NOTIFICATION_CHANNEL);
notificationManager.createNotificationChannel(messagesChannel);
final NotificationChannel silentMessagesChannel =
new NotificationChannel(
@ -295,6 +276,41 @@ public class NotificationService {
notificationManager.createNotificationChannel(deliveryFailedChannel);
}
@RequiresApi(api = Build.VERSION_CODES.R)
public static void createConversationChannel(
final Context context, final ShortcutInfoCompat shortcut) {
final var messagesChannel = prepareMessagesChannel(context, UUID.randomUUID().toString());
messagesChannel.setName(shortcut.getShortLabel());
messagesChannel.setConversationId(MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
final var notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(messagesChannel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannel prepareMessagesChannel(
final Context context, final String id) {
final NotificationChannel messagesChannel =
new NotificationChannel(
id,
context.getString(R.string.messages_channel_name),
NotificationManager.IMPORTANCE_HIGH);
messagesChannel.setShowBadge(true);
messagesChannel.setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build());
messagesChannel.setLightColor(LED_COLOR);
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
messagesChannel.setVibrationPattern(pattern);
messagesChannel.enableVibration(true);
messagesChannel.enableLights(true);
messagesChannel.setGroup("chats");
return messagesChannel;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static void createInitialIncomingCallChannelIfNecessary(final Context context) {
final var currentIteration = getCurrentIncomingCallChannelIteration(context);
@ -562,7 +578,8 @@ public class NotificationService {
Log.d(
Config.LOGTAG,
message.getConversation().getAccount().getJid().asBareJid()
+ ": suppressing failed delivery notification because conversation is open");
+ ": suppressing failed delivery notification because conversation is"
+ " open");
return;
}
final PendingIntent pendingIntent = createContentIntent(conversation);
@ -640,7 +657,7 @@ public class NotificationService {
if (mXmppConnectionService.getBooleanPreference("app_lock_enabled", R.bool.app_lock_enabled)) {
final Contact contact = id.getContact();
builder.addPerson(getPerson(contact));
ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfo(contact);
builder.setShortcutInfo(info);
if (Build.VERSION.SDK_INT >= 30) {
mXmppConnectionService.getSystemService(ShortcutManager.class).pushDynamicShortcut(info.toShortcutInfo());
@ -717,7 +734,7 @@ public class NotificationService {
} else {
final Contact contact = id.getContact();
builder.addPerson(getPerson(contact));
ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfo(contact);
builder.setShortcutInfo(info);
if (Build.VERSION.SDK_INT >= 30) {
mXmppConnectionService.getSystemService(ShortcutManager.class).pushDynamicShortcut(info.toShortcutInfo());
@ -874,7 +891,8 @@ public class NotificationService {
if (jingleRtpConnection == null) {
return false;
}
final var notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class);
final var notificationManager =
mXmppConnectionService.getSystemService(NotificationManager.class);
if (Iterables.any(
Arrays.asList(notificationManager.getActiveNotifications()),
n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) {
@ -988,7 +1006,8 @@ public class NotificationService {
Log.d(
Config.LOGTAG,
conversational.getAccount().getJid().asBareJid()
+ ": dismissed missed call because call was picked up on other device");
+ ": dismissed missed call because call was picked up on"
+ " other device");
iterator.remove();
}
}
@ -1512,12 +1531,12 @@ public class NotificationService {
if (systemAccount != null) {
notificationBuilder.addPerson(systemAccount.toString());
}
info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
info = mXmppConnectionService.getShortcutService().getShortcutInfo(contact);
} else {
info =
mXmppConnectionService
.getShortcutService()
.getShortcutInfoCompat(conversation.getMucOptions());
.getShortcutInfo(conversation.getMucOptions());
}
notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
notificationBuilder.setSmallIcon(R.drawable.ic_notification);
@ -1669,12 +1688,15 @@ public class NotificationService {
if (systemAccount != null) {
notificationBuilder.addPerson(systemAccount.toString());
}
info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
info =
mXmppConnectionService
.getShortcutService()
.getShortcutInfo(contact, conversation.getUuid());
} else {
info =
mXmppConnectionService
.getShortcutService()
.getShortcutInfoCompat(conversation.getMucOptions());
.getShortcutInfo(conversation.getMucOptions());
}
notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
notificationBuilder.setSmallIcon(R.drawable.ic_notification);
@ -1710,16 +1732,16 @@ public class NotificationService {
}
final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
bigPictureStyle.bigPicture(bitmap);
if (tmp.size() > 0) {
CharSequence text = getMergedBodies(tmp);
bigPictureStyle.setSummaryText(text);
builder.setContentText(text);
builder.setTicker(text);
} else {
if (tmp.isEmpty()) {
final String description =
UIHelper.getFileDescriptionString(mXmppConnectionService, message);
builder.setContentText(description);
builder.setTicker(description);
} else {
final CharSequence text = getMergedBodies(tmp);
bigPictureStyle.setSummaryText(text);
builder.setContentText(text);
builder.setTicker(text);
}
builder.setStyle(bigPictureStyle);
} catch (final IOException e) {

View file

@ -2,17 +2,15 @@ package eu.siacs.conversations.services;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.os.PersistableBundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import java.util.ArrayList;
@ -20,21 +18,30 @@ import java.util.HashMap;
import java.util.List;
import java.util.Set;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.StartConversationActivity;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
import eu.siacs.conversations.xmpp.Jid;
import java.util.Collection;
import java.util.List;
public class ShortcutService {
private final XmppConnectionService xmppConnectionService;
private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor =
new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
public ShortcutService(XmppConnectionService xmppConnectionService) {
public ShortcutService(final XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService;
}
@ -44,12 +51,7 @@ public class ShortcutService {
public void refresh(final boolean forceUpdate) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
final Runnable r = new Runnable() {
@Override
public void run() {
refreshImpl(forceUpdate);
}
};
final Runnable r = () -> refreshImpl(forceUpdate);
replacingSerialSingleThreadExecutor.execute(r);
}
}
@ -57,71 +59,94 @@ public class ShortcutService {
@TargetApi(25)
public void report(Contact contact) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class);
ShortcutManager shortcutManager =
xmppConnectionService.getSystemService(ShortcutManager.class);
shortcutManager.reportShortcutUsed(getShortcutId(contact));
}
}
@TargetApi(25)
private void refreshImpl(boolean forceUpdate) {
List<FrequentContact> frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30);
HashMap<String,Account> accounts = new HashMap<>();
for(Account account : xmppConnectionService.getAccounts()) {
accounts.put(account.getUuid(),account);
}
List<Contact> contacts = new ArrayList<>();
for(FrequentContact frequentContact : frequentContacts) {
Account account = accounts.get(frequentContact.account);
private void refreshImpl(final boolean forceUpdate) {
final var frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30);
final var accounts =
ImmutableMap.copyOf(
Maps.uniqueIndex(xmppConnectionService.getAccounts(), Account::getUuid));
final var contactBuilder = new ImmutableMap.Builder<FrequentContact, Contact>();
for (final var frequentContact : frequentContacts) {
final Account account = accounts.get(frequentContact.account);
if (account != null) {
contacts.add(account.getRoster().getContact(frequentContact.contact));
final var contact = account.getRoster().getContact(frequentContact.contact);
contactBuilder.put(frequentContact, contact);
}
}
ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class);
boolean needsUpdate = forceUpdate || contactsChanged(contacts,shortcutManager.getDynamicShortcuts());
final var contacts = contactBuilder.build();
final var current = ShortcutManagerCompat.getDynamicShortcuts(xmppConnectionService);
boolean needsUpdate = forceUpdate || contactsChanged(contacts.values(), current);
if (!needsUpdate) {
Log.d(Config.LOGTAG,"skipping shortcut update");
Log.d(Config.LOGTAG, "skipping shortcut update");
return;
}
List<ShortcutInfo> newDynamicShortCuts = new ArrayList<>();
for (Contact contact : contacts) {
ShortcutInfo shortcut = getShortcutInfo(contact);
newDynamicShortCuts.add(shortcut);
final var newDynamicShortcuts = new ImmutableList.Builder<ShortcutInfoCompat>();
for (final var entry : contacts.entrySet()) {
final var contact = entry.getValue();
final var conversation = entry.getKey().conversation;
final var shortcut = getShortcutInfo(contact, conversation);
newDynamicShortcuts.add(shortcut);
}
if (shortcutManager.setDynamicShortcuts(newDynamicShortCuts)) {
Log.d(Config.LOGTAG,"updated dynamic shortcuts");
if (ShortcutManagerCompat.setDynamicShortcuts(
xmppConnectionService, newDynamicShortcuts.build())) {
Log.d(Config.LOGTAG, "updated dynamic shortcuts");
} else {
Log.d(Config.LOGTAG, "unable to update dynamic shortcuts");
}
}
public ShortcutInfoCompat getShortcutInfoCompat(Contact contact) {
return new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact))
public ShortcutInfoCompat getShortcutInfo(final Contact contact) {
final var conversation = xmppConnectionService.find(contact);
final var uuid = conversation == null ? null : conversation.getUuid();
return getShortcutInfo(contact, uuid);
}
public ShortcutInfoCompat getShortcutInfo(final Contact contact, final String conversation) {
final ShortcutInfoCompat.Builder builder =
new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact))
.setShortLabel(contact.getDisplayName())
.setIntent(getShortcutIntent(contact))
.setIcon(IconCompat.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(contact)))
.setIsConversation()
.setCategories(Set.of("de.monocles.chat.SHARE_TARGET"))
.build();
.setIsConversation();
builder.setIcon(
IconCompat.createWithBitmap(
xmppConnectionService.getAvatarService().getRoundedShortcut(contact)));
if (conversation != null) {
setConversation(builder, conversation);
}
return builder.build();
}
public ShortcutInfoCompat getShortcutInfoCompat(final MucOptions mucOptions) {
return new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
public ShortcutInfoCompat getShortcutInfo(final MucOptions mucOptions) {
final ShortcutInfoCompat.Builder builder =
new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
.setShortLabel(mucOptions.getConversation().getName())
.setIntent(getShortcutIntent(mucOptions))
.setIcon(IconCompat.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions)))
.setIsConversation()
.setCategories(Set.of("de.monocles.chat.SHARE_TARGET"))
.build();
.setIsConversation();
builder.setIcon(
IconCompat.createWithBitmap(
xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions)));
setConversation(builder, mucOptions.getConversation().getUuid());
return builder.build();
}
@TargetApi(Build.VERSION_CODES.N_MR1)
private ShortcutInfo getShortcutInfo(final Contact contact) {
return getShortcutInfoCompat(contact).toShortcutInfo();
private static void setConversation(
final ShortcutInfoCompat.Builder builder, @NonNull final String conversation) {
builder.setCategories(ImmutableSet.of("eu.siacs.conversations.category.SHARE_TARGET"));
final var extras = new PersistableBundle();
extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation);
builder.setExtras(extras);
}
private static boolean contactsChanged(List<Contact> needles, List<ShortcutInfo> haystack) {
for(Contact needle : needles) {
if(!contactExists(needle,haystack)) {
private static boolean contactsChanged(
final Collection<Contact> needles, final List<ShortcutInfoCompat> haystack) {
for (final Contact needle : needles) {
if (!contactExists(needle, haystack)) {
return true;
}
}
@ -129,17 +154,22 @@ public class ShortcutService {
}
@TargetApi(25)
private static boolean contactExists(Contact needle, List<ShortcutInfo> haystack) {
for(ShortcutInfo shortcutInfo : haystack) {
if (getShortcutId(needle).equals(shortcutInfo.getId()) && needle.getDisplayName().equals(shortcutInfo.getShortLabel())) {
private static boolean contactExists(
final Contact needle, final List<ShortcutInfoCompat> haystack) {
for (final ShortcutInfoCompat shortcutInfo : haystack) {
final var label = shortcutInfo.getShortLabel();
if (getShortcutId(needle).equals(shortcutInfo.getId())
&& needle.getDisplayName().equals(label.toString())) {
return true;
}
}
return false;
}
private static String getShortcutId(Contact contact) {
return contact.getAccount().getJid().asBareJid().toEscapedString()+"#"+contact.getJid().asBareJid().toEscapedString();
private static String getShortcutId(final Contact contact) {
return contact.getAccount().getJid().asBareJid().toEscapedString()
+ "#"
+ contact.getJid().asBareJid().toEscapedString();
}
private static String getShortcutId(final MucOptions mucOptions) {
@ -180,12 +210,13 @@ public class ShortcutService {
}
@NonNull
public Intent createShortcut(Contact contact, boolean legacy) {
public Intent createShortcut(final Contact contact, final boolean legacy) {
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !legacy) {
ShortcutInfo shortcut = getShortcutInfo(contact);
ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class);
intent = shortcutManager.createShortcutResultIntent(shortcut);
final var shortcut = getShortcutInfo(contact);
intent =
ShortcutManagerCompat.createShortcutResultIntent(
xmppConnectionService, shortcut);
} else {
intent = createShortcutResultIntent(contact);
}
@ -193,7 +224,7 @@ public class ShortcutService {
}
@NonNull
private Intent createShortcutResultIntent(Contact contact) {
private Intent createShortcutResultIntent(final Contact contact) {
AvatarService avatarService = xmppConnectionService.getAvatarService();
Bitmap icon = avatarService.getRoundedShortcutWithIcon(contact);
Intent intent = new Intent();
@ -204,13 +235,14 @@ public class ShortcutService {
}
public static class FrequentContact {
private final String conversation;
private final String account;
private final Jid contact;
public FrequentContact(String account, Jid contact) {
public FrequentContact(final String conversation, final String account, final Jid contact) {
this.conversation = conversation;
this.account = account;
this.contact = contact;
}
}
}

View file

@ -9,14 +9,12 @@ import de.monocles.chat.EmojiSearch;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Conversations;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Reaction;
import eu.siacs.conversations.utils.UIHelper;
@ -25,32 +23,34 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.function.Function;
public class BindingAdapters {
public static void setReactionsOnReceived(
final ChipGroup chipGroup,
final Conversation conversation,
final Reaction.Aggregated reactions,
final Consumer<Collection<String>> onModifiedReactions,
final Function<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>, Boolean> onDetailsClicked,
final Consumer<EmojiSearch.CustomEmoji> onCustomReaction,
final Consumer<Reaction> onCustomReactionRemove,
final Runnable addReaction) {
setReactions(chipGroup, conversation, reactions, true, onModifiedReactions, onCustomReaction, onCustomReactionRemove, addReaction);
setReactions(chipGroup, reactions, true, onModifiedReactions, onDetailsClicked, onCustomReaction, onCustomReactionRemove, addReaction);
}
public static void setReactionsOnSent(
final ChipGroup chipGroup,
final Reaction.Aggregated reactions,
final Consumer<Collection<String>> onModifiedReactions) {
setReactions(chipGroup, null, reactions, false, onModifiedReactions, null, null, null);
final Consumer<Collection<String>> onModifiedReactions,
final Function<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>, Boolean> onDetailsClicked) {
setReactions(chipGroup, reactions, false, onModifiedReactions, onDetailsClicked, null, null, null);
}
private static void setReactions(
final ChipGroup chipGroup,
final Conversation conversation,
final Reaction.Aggregated aggregated,
final boolean onReceived,
final Consumer<Collection<String>> onModifiedReactions,
final Function<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>, Boolean> onDetailsClicked,
final Consumer<EmojiSearch.CustomEmoji> onCustomReaction,
final Consumer<Reaction> onCustomReactionRemove,
final Runnable addReaction) {
@ -123,14 +123,7 @@ public class BindingAdapters {
}
}
});
chip.setOnLongClickListener(v -> {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(emoji.toString());
builder.setMessage(reaction.getValue().stream().map(r -> UIHelper.getDisplayName(conversation, r)).collect(Collectors.joining("\n")));
builder.setPositiveButton(context.getResources().getString(R.string.ok), null);
builder.create().show();
return true;
});
chip.setOnLongClickListener(v -> onDetailsClicked.apply(reaction));
chipGroup.addView(chip);
}
if (addReaction != null) {

View file

@ -1,6 +1,5 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -17,11 +16,9 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.base.Strings;
@ -35,7 +32,6 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Room;
import eu.siacs.conversations.services.ChannelDiscoveryService;
@ -46,7 +42,11 @@ import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.xmpp.Jid;
public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.OnActionExpandListener, TextView.OnEditorActionListener, ChannelDiscoveryService.OnChannelSearchResultsFound, ChannelSearchResultAdapter.OnChannelSearchResultSelected {
public class ChannelDiscoveryActivity extends XmppActivity
implements MenuItem.OnActionExpandListener,
TextView.OnEditorActionListener,
ChannelDiscoveryService.OnChannelSearchResultsFound,
ChannelSearchResultAdapter.OnChannelSearchResultSelected {
private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in";
@ -63,9 +63,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
private boolean optedIn = false;
@Override
protected void refreshUiReal() {
}
protected void refreshUiReal() {}
@Override
protected void onBackendConnected() {
@ -101,7 +99,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
this.adapter.setOnChannelSearchResultSelectedListener(this);
this.optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false);
final String search = savedInstanceState == null ? null : savedInstanceState.getString("search");
final String search =
savedInstanceState == null ? null : savedInstanceState.getString("search");
if (search != null) {
mInitialSearchValue.push(search);
}
@ -111,14 +110,17 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
private ChannelDiscoveryService.Method getMethod(final Context c) {
if (this.mucServices != null) return ChannelDiscoveryService.Method.LOCAL_SERVER;
if ( Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
return ChannelDiscoveryService.Method.LOCAL_SERVER;
}
if (QuickConversationsService.isQuicksy()) {
return ChannelDiscoveryService.Method.JABBER_NETWORK;
}
final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(c);
final String m = p.getString("channel_discovery_method", c.getString(R.string.default_channel_discovery));
final String m =
p.getString(
"channel_discovery_method",
c.getString(R.string.default_channel_discovery));
try {
return ChannelDiscoveryService.Method.valueOf(m);
} catch (IllegalArgumentException e) {
@ -139,7 +141,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
mMenuSearchView.expandActionView();
mSearchEditText.append(initialSearchValue);
mSearchEditText.requestFocus();
if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) && xmppConnectionService != null) {
if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER)
&& xmppConnectionService != null) {
xmppConnectionService.discoverChannels(initialSearchValue, this.method, this.mucServices, this);
}
}
@ -150,18 +153,22 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
@Override
public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
mSearchEditText.post(() -> {
mSearchEditText.requestFocus();
final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
});
mSearchEditText.post(
() -> {
mSearchEditText.requestFocus();
final InputMethodManager imm =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
});
return true;
}
@Override
public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
final InputMethodManager imm =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(
mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
mSearchEditText.setText("");
toggleLoadingScreen();
if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) {
@ -173,7 +180,9 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
private void toggleLoadingScreen() {
adapter.submitList(Collections.emptyList());
binding.progressBar.setVisibility(View.VISIBLE);
binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface));
binding.list.setBackgroundColor(
MaterialColors.getColor(
binding.list, com.google.android.material.R.attr.colorSurface));
}
@Override
@ -188,13 +197,14 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn());
builder.setOnCancelListener(dialog -> finish());
final androidx.appcompat.app.AlertDialog dialog = builder.create();
dialog.setOnShowListener(d -> {
final TextView textView = dialog.findViewById(android.R.id.message);
if (textView == null) {
return;
}
textView.setMovementMethod(LinkMovementMethod.getInstance());
});
dialog.setOnShowListener(
d -> {
final TextView textView = dialog.findViewById(android.R.id.message);
if (textView == null) {
return;
}
textView.setMovementMethod(LinkMovementMethod.getInstance());
});
dialog.setCanceledOnTouchOutside(false);
dialog.show();
holdLoading();
@ -204,13 +214,17 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
private void holdLoading() {
adapter.submitList(Collections.emptyList());
binding.progressBar.setVisibility(View.GONE);
binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface));
binding.list.setBackgroundColor(
MaterialColors.getColor(
binding.list, com.google.android.material.R.attr.colorSurface));
}
@Override
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
savedInstanceState.putString(
"search",
mSearchEditText != null ? mSearchEditText.getText().toString() : null);
}
super.onSaveInstanceState(savedInstanceState);
}
@ -235,16 +249,20 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
@Override
public void onChannelSearchResultsFound(final List<Room> results) {
runOnUiThread(() -> {
adapter.submitList(results);
binding.progressBar.setVisibility(View.GONE);
if (results.isEmpty()) {
binding.list.setBackground(ContextCompat.getDrawable(this,R.drawable.background_no_results));
} else {
binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface));
}
});
runOnUiThread(
() -> {
adapter.submitList(results);
binding.progressBar.setVisibility(View.GONE);
if (results.isEmpty()) {
binding.list.setBackground(
ContextCompat.getDrawable(this, R.drawable.background_no_results));
} else {
binding.list.setBackgroundColor(
MaterialColors.getColor(
binding.list,
com.google.android.material.R.attr.colorSurface));
}
});
}
@Override
@ -258,12 +276,16 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
final AtomicReference<String> account = new AtomicReference<>(accounts.get(0));
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.choose_account);
builder.setSingleChoiceItems(accounts.toArray(new CharSequence[0]), 0, (dialog, which) -> account.set(accounts.get(which)));
builder.setPositiveButton(R.string.join, (dialog, which) -> joinChannelSearchResult(account.get(), result));
builder.setSingleChoiceItems(
accounts.toArray(new CharSequence[0]),
0,
(dialog, which) -> account.set(accounts.get(which)));
builder.setPositiveButton(
R.string.join,
(dialog, which) -> joinChannelSearchResult(account.get(), result));
builder.setNegativeButton(R.string.cancel, null);
builder.create().show();
}
}
@Override
@ -294,17 +316,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
final Conversation conversation =
xmppConnectionService.findOrCreateConversation(
account, result.getRoom(), true, true, true);
final var existingBookmark = conversation.getBookmark();
if (existingBookmark == null) {
final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
bookmark.setAutojoin(true);
xmppConnectionService.createBookmark(account, bookmark);
} else {
if (!existingBookmark.autojoin()) {
existingBookmark.setAutojoin(true);
xmppConnectionService.createBookmark(account, existingBookmark);
}
}
xmppConnectionService.ensureBookmarkIsAutoJoin(conversation);
switchToConversation(conversation);
}
}

View file

@ -1,5 +1,8 @@
package eu.siacs.conversations.ui;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
import static eu.siacs.conversations.utils.StringUtils.changed;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
@ -7,6 +10,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
@ -31,6 +35,7 @@ import androidx.databinding.DataBindingUtil;
import de.monocles.chat.Util;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.color.MaterialColors;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
@ -77,14 +82,17 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.utils.XEP0392Helper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import me.drakeet.support.toast.ToastCompat;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
import static eu.siacs.conversations.utils.StringUtils.changed;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnConfigurationPushed, XmppConnectionService.OnRoomDestroy, TextWatcher, OnMediaLoaded {
public class ConferenceDetailsActivity extends XmppActivity
implements OnConversationUpdate,
OnMucRosterUpdate,
XmppConnectionService.OnAffiliationChanged,
XmppConnectionService.OnConfigurationPushed,
XmppConnectionService.OnRoomDestroy,
TextWatcher,
OnMediaLoaded {
public static final String ACTION_VIEW_MUC = "view_muc";
private Conversation mConversation;
@ -96,26 +104,25 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
private boolean mAdvancedMode = false;
private boolean showDynamicTags = true;
private final UiCallback<Conversation> renameCallback = new UiCallback<Conversation>() {
@Override
public void success(Conversation object) {
displayToast(getString(R.string.your_nick_has_been_changed));
runOnUiThread(() -> {
updateView();
});
private final UiCallback<Conversation> renameCallback =
new UiCallback<Conversation>() {
@Override
public void success(Conversation object) {
displayToast(getString(R.string.your_nick_has_been_changed));
runOnUiThread(
() -> {
updateView();
});
}
}
@Override
public void error(final int errorCode, Conversation object) {
displayToast(getString(errorCode));
}
@Override
public void error(final int errorCode, Conversation object) {
displayToast(getString(errorCode));
}
@Override
public void userInputRequired(PendingIntent pi, Conversation object) {
}
};
@Override
public void userInputRequired(PendingIntent pi, Conversation object) {}
};
public static void open(final Activity activity, final Conversation conversation) {
Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
@ -124,39 +131,49 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
activity.startActivity(intent);
}
private final OnClickListener mNotifyStatusClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
builder.setTitle(R.string.pref_notification_settings);
String[] choices = {
getString(R.string.notify_on_all_messages),
getString(R.string.notify_only_when_highlighted),
private final OnClickListener mNotifyStatusClickListener =
new OnClickListener() {
@Override
public void onClick(View v) {
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
builder.setTitle(R.string.pref_notification_settings);
String[] choices = {
getString(R.string.notify_on_all_messages),
getString(R.string.notify_only_when_highlighted),
getString(R.string.notify_only_when_highlighted_or_replied),
getString(R.string.notify_never)
};
final AtomicInteger choice;
if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0) == Long.MAX_VALUE) {
choice = new AtomicInteger(3);
} else {
choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : (mConversation.notifyReplies() ? 2 : 1));
}
builder.setSingleChoiceItems(choices, choice.get(), (dialog, which) -> choice.set(which));
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.ok, (dialog, which) -> {
if (choice.get() == 3) {
mConversation.setMutedTill(Long.MAX_VALUE);
} else {
mConversation.setMutedTill(0);
mConversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, String.valueOf(choice.get() == 0));
mConversation.setAttribute(Conversation.ATTRIBUTE_NOTIFY_REPLIES, String.valueOf(choice.get() == 2));
getString(R.string.notify_never)
};
final AtomicInteger choice;
if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0)
== Long.MAX_VALUE) {
choice = new AtomicInteger(3);
} else {
choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : (mConversation.notifyReplies() ? 2 : 1));
}
builder.setSingleChoiceItems(
choices, choice.get(), (dialog, which) -> choice.set(which));
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(
R.string.ok,
(dialog, which) -> {
if (choice.get() == 3) {
mConversation.setMutedTill(Long.MAX_VALUE);
} else {
mConversation.setMutedTill(0);
mConversation.setAttribute(
Conversation.ATTRIBUTE_ALWAYS_NOTIFY,
String.valueOf(choice.get() == 0));
mConversation.setAttribute(
Conversation.ATTRIBUTE_NOTIFY_REPLIES,
String.valueOf(choice.get() == 2));
}
xmppConnectionService.updateConversation(mConversation);
updateView();
});
builder.create().show();
}
xmppConnectionService.updateConversation(mConversation);
updateView();
});
builder.create().show();
}
};
};
private final OnClickListener mChangeConferenceSettings =
new OnClickListener() {
@ -217,32 +234,49 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
this.binding.editNickButton.setOnClickListener(v -> quickEdit(mConversation.getMucOptions().getActualNick(),
R.string.nickname,
value -> {
if (xmppConnectionService.renameInMuc(mConversation, value, renameCallback)) {
return null;
} else {
return getString(R.string.invalid_muc_nick);
}
}));
this.binding.editNickButton.setOnClickListener(
v ->
quickEdit(
mConversation.getMucOptions().getActualNick(),
R.string.nickname,
value -> {
if (xmppConnectionService.renameInMuc(
mConversation, value, renameCallback)) {
return null;
} else {
return getString(R.string.invalid_muc_nick);
}
}));
this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
this.binding.yourPhoto.setOnClickListener(v -> {
final MucOptions mucOptions = mConversation.getMucOptions();
if (!mucOptions.hasVCards()) {
Toast.makeText(this, R.string.host_does_not_support_group_chat_avatars, Toast.LENGTH_SHORT).show();
return;
}
if (!mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
Toast.makeText(this, R.string.only_the_owner_can_change_group_chat_avatar, Toast.LENGTH_SHORT).show();
return;
}
final Intent intent = new Intent(this, PublishGroupChatProfilePictureActivity.class);
intent.putExtra("uuid", mConversation.getUuid());
startActivity(intent);
});
this.binding.yourPhoto.setOnClickListener(
v -> {
final MucOptions mucOptions = mConversation.getMucOptions();
if (!mucOptions.hasVCards()) {
Toast.makeText(
this,
R.string.host_does_not_support_group_chat_avatars,
Toast.LENGTH_SHORT)
.show();
return;
}
if (!mucOptions
.getSelf()
.getAffiliation()
.ranks(MucOptions.Affiliation.OWNER)) {
Toast.makeText(
this,
R.string.only_the_owner_can_change_group_chat_avatar,
Toast.LENGTH_SHORT)
.show();
return;
}
final Intent intent =
new Intent(this, PublishGroupChatProfilePictureActivity.class);
intent.putExtra("uuid", mConversation.getUuid());
startActivity(intent);
});
this.binding.yourPhoto.setOnLongClickListener(v -> {
PopupMenu popupMenu = new PopupMenu(this, v);
popupMenu.inflate(R.menu.conference_photo);
@ -270,11 +304,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
popupMenu.show();
return true;
});
this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic));
this.binding.editMucNameButton.setContentDescription(
getString(R.string.edit_name_and_topic));
this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
this.binding.mucEditTitle.addTextChangedListener(this);
this.binding.mucEditSubject.addTextChangedListener(this);
//this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
//this.binding.mucEditSubject.addTextChangedListener(
// new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
this.binding.editTags.addTextChangedListener(this);
this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
this.mUserPreviewAdapter = new UserPreviewAdapter();
@ -287,11 +323,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
switchToConversation(mConversation, null, false, null, false, true, null, thread.getThreadId());
});
this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation));
this.binding.showUsers.setOnClickListener(v -> {
Intent intent = new Intent(this, MucUsersActivity.class);
intent.putExtra("uuid", mConversation.getUuid());
startActivity(intent);
});
this.binding.showUsers.setOnClickListener(
v -> {
Intent intent = new Intent(this, MucUsersActivity.class);
intent.putExtra("uuid", mConversation.getUuid());
startActivity(intent);
});
this.binding.relatedMucs.setOnClickListener(v -> {
final Intent intent = new Intent(this, ChannelDiscoveryActivity.class);
intent.putExtra("services", new String[]{ mConversation.getJid().getDomain().toEscapedString(), mConversation.getAccount().getJid().toEscapedString() });
@ -302,7 +339,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public void onStart() {
super.onStart();
binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
binding.mediaWrapper.setVisibility(
Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
}
@Override
@ -330,23 +368,43 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.mAdvancedMode = !menuItem.isChecked();
menuItem.setChecked(this.mAdvancedMode);
getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply();
final boolean online = mConversation != null && mConversation.getMucOptions().online();
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
final boolean online =
mConversation != null && mConversation.getMucOptions().online();
this.binding.mucInfoMore.setVisibility(
this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
invalidateOptionsMenu();
updateView();
break;
case R.id.action_custom_notifications:
if (mConversation != null) {
configureCustomNotifications(mConversation);
}
break;
}
return super.onOptionsItemSelected(menuItem);
}
private void configureCustomNotifications(final Conversation conversation) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|| conversation.getMode() != Conversation.MODE_MULTI) {
return;
}
final var shortcut =
xmppConnectionService
.getShortcutService()
.getShortcutInfo(conversation.getMucOptions());
configureCustomNotification(shortcut);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
public boolean onContextItemSelected(@NonNull final MenuItem item) {
final User user = mUserPreviewAdapter.getSelectedUser();
if (user == null) {
Toast.makeText(this, R.string.unable_to_perform_this_action, Toast.LENGTH_SHORT).show();
return true;
}
if (!MucDetailsContextMenuHelper.onContextItemSelected(item, mUserPreviewAdapter.getSelectedUser(), this)) {
if (!MucDetailsContextMenuHelper.onContextItemSelected(
item, mUserPreviewAdapter.getSelectedUser(), this)) {
return super.onContextItemSelected(item);
}
return true;
@ -361,7 +419,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
final String name = mucOptions.getName();
this.binding.mucEditTitle.setText("");
final boolean owner = mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
final boolean owner =
mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
if (owner || printableValue(name)) {
this.binding.mucEditTitle.setVisibility(View.VISIBLE);
if (name != null) {
@ -413,8 +472,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.editTags.setVisibility(View.GONE);
}
} else {
String subject = this.binding.mucEditSubject.isEnabled() ? this.binding.mucEditSubject.getEditableText().toString().trim() : null;
String name = this.binding.mucEditTitle.isEnabled() ? this.binding.mucEditTitle.getEditableText().toString().trim() : null;
String subject =
this.binding.mucEditSubject.isEnabled()
? this.binding.mucEditSubject.getEditableText().toString().trim()
: null;
String name =
this.binding.mucEditTitle.isEnabled()
? this.binding.mucEditTitle.getEditableText().toString().trim()
: null;
onMucInfoUpdated(subject, name);
final Bookmark bookmark = mConversation.getBookmark();
@ -433,7 +498,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.mucEditor.setVisibility(View.GONE);
this.binding.mucDisplay.setVisibility(View.VISIBLE);
this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic));
this.binding.editMucNameButton.setContentDescription(
getString(R.string.edit_name_and_topic));
}
private void onMucInfoUpdated(String subject, String name) {
@ -441,7 +507,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
xmppConnectionService.pushSubjectToConference(mConversation, subject);
}
if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) && changed(mucOptions.getName(), name)) {
if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)
&& changed(mucOptions.getName(), name)) {
Bundle options = new Bundle();
options.putString("muc#roomconfig_persistentroom", "1");
options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name));
@ -449,12 +516,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
}
@Override
protected String getShareableUri(boolean http) {
if (mConversation != null) {
if (http) {
return "https://conversations.im/j/" + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toEscapedString());
return "https://conversations.im/j/"
+ XmppUri.lameUrlEncode(
mConversation.getJid().asBareJid().toEscapedString());
} else {
return "xmpp:" + Uri.encode(mConversation.getJid().asBareJid().toEscapedString(), "@/+") + "?join";
}
@ -473,7 +541,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
return true;
}
menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
menuItemDestroyRoom.setVisible(mConversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER));
menuItemDestroyRoom.setVisible(
mConversation
.getMucOptions()
.getSelf()
.getAffiliation()
.ranks(MucOptions.Affiliation.OWNER));
return true;
}
@ -486,32 +559,40 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final MenuItem destroy = menu.findItem(R.id.action_destroy_room);
destroy.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
AccountUtils.showHideMenuItems(menu);
final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
if (customNotifications != null) customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
return super.onCreateOptionsMenu(menu);
}
@Override
public void onMediaLoaded(List<Attachment> attachments) {
runOnUiThread(() -> {
int limit = GridManager.getCurrentColumnCount(binding.media);
mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
});
public void onMediaLoaded(final List<Attachment> attachments) {
runOnUiThread(
() -> {
final int limit = GridManager.getCurrentColumnCount(binding.media);
mMediaAdapter.setAttachments(
attachments.subList(0, Math.min(limit, attachments.size())));
binding.mediaWrapper.setVisibility(
attachments.isEmpty() ? View.GONE : View.VISIBLE);
});
}
protected void saveAsBookmark() {
xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName());
xmppConnectionService.saveConversationAsBookmark(
mConversation, mConversation.getMucOptions().getName());
}
protected void destroyRoom() {
final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
builder.setMessage(groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
builder.setPositiveButton(R.string.ok, (dialog, which) -> {
xmppConnectionService.destroyRoom(mConversation, ConferenceDetailsActivity.this);
});
builder.setMessage(
groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
builder.setPositiveButton(
R.string.ok,
(dialog, which) -> {
xmppConnectionService.destroyRoom(
mConversation, ConferenceDetailsActivity.this);
});
builder.setNegativeButton(R.string.cancel, null);
final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
@ -533,7 +614,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
if (Compatibility.hasStoragePermission(this)) {
final int limit = GridManager.getCurrentColumnCount(this.binding.media);
xmppConnectionService.getAttachments(this.mConversation, limit, this);
this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, mConversation));
this.binding.showMedia.setOnClickListener(
(v) -> MediaBrowserActivity.launch(this, mConversation));
}
if (xmppConnectionService != null && xmppConnectionService.getBooleanPreference("default_store_media_in_cache", R.bool.default_store_media_in_cache)) {
@ -572,20 +654,25 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final MucOptions mucOptions = mConversation.getMucOptions();
final User self = mucOptions.getSelf();
final String account = mConversation.getAccount().getJid().asBareJid().toEscapedString();
setTitle(mucOptions.isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details);
setTitle(
mucOptions.isPrivateAndNonAnonymous()
? R.string.action_muc_details
: R.string.channel_details);
final Bookmark bookmark = mConversation.getBookmark();
final XmppConnection connection = mConversation.getAccount().getXmppConnection();
this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject() || (bookmark != null && connection != null && connection.getFeatures().bookmarks2())) ? View.VISIBLE : View.GONE);
this.binding.detailsAccount.setText(getString(R.string.using_account, account));
this.binding.truejid.setVisibility(View.GONE);
if (mConversation.isPrivateAndNonAnonymous()) {
this.binding.jid.setText(getString(R.string.hosted_on, mConversation.getJid().getDomain()));
this.binding.jid.setText(
getString(R.string.hosted_on, mConversation.getJid().getDomain()));
this.binding.truejid.setText(mConversation.getJid().asBareJid().toEscapedString());
if (mAdvancedMode) this.binding.truejid.setVisibility(View.VISIBLE);
} else {
this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString());
}
AvatarWorkerTask.loadAvatar(mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
AvatarWorkerTask.loadAvatar(
mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
String roomName = mucOptions.getName();
String subject = mucOptions.getSubject();
final boolean hasTitle;
@ -606,7 +693,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
MyLinkify.addLinks(spannable, false);
this.binding.mucSubject.setText(spannable);
this.binding.mucSubject.setTextAppearance( subject.length() > (hasTitle ? 128 : 196) ? com.google.android.material.R.style.TextAppearance_Material3_BodyMedium : com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
this.binding.mucSubject.setTextAppearance(
subject.length() > (hasTitle ? 128 : 196)
? com.google.android.material.R.style
.TextAppearance_Material3_BodyMedium
: com.google.android.material.R.style
.TextAppearance_Material3_BodyLarge);
this.binding.mucSubject.setAutoLinkMask(0);
this.binding.mucSubject.setVisibility(View.VISIBLE);
this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
@ -624,7 +716,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
} else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
this.binding.mucSettings.setVisibility(View.VISIBLE);
this.binding.mucConferenceType.setText(R.string.group_chat_will_make_your_jabber_id_public);
this.binding.mucConferenceType.setText(
R.string.group_chat_will_make_your_jabber_id_public);
} else {
this.binding.mucSettings.setVisibility(View.GONE);
}
@ -647,43 +740,55 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
if (mutedTill == Long.MAX_VALUE) {
this.binding.notificationStatusText.setText(R.string.notify_never);
this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_off_24dp);
this.binding.notificationStatusButton.setImageResource(
R.drawable.ic_notifications_off_24dp);
} else if (System.currentTimeMillis() < mutedTill) {
this.binding.notificationStatusText.setText(R.string.notify_paused);
this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_paused_24dp);
this.binding.notificationStatusButton.setImageResource(
R.drawable.ic_notifications_paused_24dp);
} else if (mConversation.alwaysNotify()) {
this.binding.notificationStatusText.setText(R.string.notify_on_all_messages);
this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_24dp);
this.binding.notificationStatusButton.setImageResource(
R.drawable.ic_notifications_24dp);
} else if (mConversation.notifyReplies()) {
this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted_or_replied);
this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
} else {
this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
this.binding.notificationStatusButton.setImageResource(
R.drawable.ic_notifications_none_24dp);
}
final List<User> users = mucOptions.getUsers();
Collections.sort(users, (a, b) -> {
if (b.getAffiliation().outranks(a.getAffiliation())) {
return 1;
} else if (a.getAffiliation().outranks(b.getAffiliation())) {
return -1;
} else {
if (a.getAvatar() != null && b.getAvatar() == null) {
return -1;
} else if (a.getAvatar() == null && b.getAvatar() != null) {
return 1;
} else {
return a.getComparableName().compareToIgnoreCase(b.getComparableName());
}
}
});
this.mUserPreviewAdapter.submitList(MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
Collections.sort(
users,
(a, b) -> {
if (b.getAffiliation().outranks(a.getAffiliation())) {
return 1;
} else if (a.getAffiliation().outranks(b.getAffiliation())) {
return -1;
} else {
if (a.getAvatar() != null && b.getAvatar() == null) {
return -1;
} else if (a.getAvatar() == null && b.getAvatar() != null) {
return 1;
} else {
return a.getComparableName().compareToIgnoreCase(b.getComparableName());
}
}
});
this.mUserPreviewAdapter.submitList(
MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
this.binding.showUsers.setVisibility(mucOptions.getUsers(true, mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.ADMIN)).size() > 0 ? View.VISIBLE : View.GONE);
this.binding.showUsers.setText(getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
this.binding.usersWrapper.setVisibility(users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
this.binding.showUsers.setText(
getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
this.binding.usersWrapper.setVisibility(
users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
if (users.size() == 0) {
this.binding.noUsersHints.setText(mucOptions.isPrivateAndNonAnonymous() ? R.string.no_users_hint_group_chat : R.string.no_users_hint_channel);
this.binding.noUsersHints.setText(
mucOptions.isPrivateAndNonAnonymous()
? R.string.no_users_hint_group_chat
: R.string.no_users_hint_channel);
this.binding.noUsersHints.setVisibility(View.VISIBLE);
} else {
this.binding.noUsersHints.setVisibility(View.GONE);
@ -729,7 +834,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
public static String getStatus(Context context, User user, final boolean advanced) {
if (advanced) {
return String.format("%s (%s)", context.getString(user.getAffiliation().getResId()), context.getString(user.getRole().getResId()));
return String.format(
"%s (%s)",
context.getString(user.getAffiliation().getResId()),
context.getString(user.getRole().getResId()));
} else {
return context.getString(user.getAffiliation().getResId());
}
@ -757,7 +865,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public void onRoomDestroyFailed() {
final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
displayToast(getString(groupChat ? R.string.could_not_destroy_room : R.string.could_not_destroy_channel));
displayToast(
getString(
groupChat
? R.string.could_not_destroy_room
: R.string.could_not_destroy_channel));
}
@Override
@ -771,23 +883,20 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
private void displayToast(final String msg) {
runOnUiThread(() -> {
if (isFinishing()) {
return;
}
ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
});
runOnUiThread(
() -> {
if (isFinishing()) {
return;
}
ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
});
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
@ -796,8 +905,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
final MucOptions mucOptions = mConversation.getMucOptions();
if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
boolean subjectChanged = changed(binding.mucEditSubject.getEditableText().toString(), mucOptions.getSubject());
boolean nameChanged = changed(binding.mucEditTitle.getEditableText().toString(), mucOptions.getName());
boolean subjectChanged =
changed(
binding.mucEditSubject.getEditableText().toString(),
mucOptions.getSubject());
boolean nameChanged =
changed(
binding.mucEditTitle.getEditableText().toString(),
mucOptions.getName());
final Bookmark bookmark = mConversation.getBookmark();
if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) {
this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);

View file

@ -33,9 +33,7 @@ import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.databinding.DataBindingUtil;
@ -101,8 +99,17 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.openintents.openpgp.util.OpenPgpUtils;
public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded {
public class ContactDetailsActivity extends OmemoActivity
implements OnAccountUpdate,
OnRosterUpdate,
OnUpdateBlocklist,
OnKeyStatusUpdated,
OnMediaLoaded {
public static final String ACTION_VIEW_CONTACT = "view_contact";
private final int REQUEST_SYNC_CONTACTS = 0x28cf;
ActivityContactDetailsBinding binding;
@ -111,40 +118,55 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
protected MenuItem save = null;
private Contact contact;
private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
private final DialogInterface.OnClickListener removeFromRoster =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
xmppConnectionService.deleteContactOnServer(contact);
}
};
private final OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
xmppConnectionService.stopPresenceUpdatesTo(contact);
} else {
contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
@Override
public void onClick(DialogInterface dialog, int which) {
xmppConnectionService.deleteContactOnServer(contact);
}
} else {
contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact));
}
}
};
private final OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
};
private final OnCheckedChangeListener mOnSendCheckedChange =
new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().requestPresenceUpdatesFrom(contact));
} else {
xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesFrom(contact));
}
}
};
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
xmppConnectionService.stopPresenceUpdatesTo(contact);
} else {
contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
}
} else {
contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
xmppConnectionService.sendPresencePacket(
contact.getAccount(),
xmppConnectionService
.getPresenceGenerator()
.stopPresenceUpdatesTo(contact));
}
}
};
private final OnCheckedChangeListener mOnReceiveCheckedChange =
new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
xmppConnectionService.sendPresencePacket(
contact.getAccount(),
xmppConnectionService
.getPresenceGenerator()
.requestPresenceUpdatesFrom(contact));
} else {
xmppConnectionService.sendPresencePacket(
contact.getAccount(),
xmppConnectionService
.getPresenceGenerator()
.stopPresenceUpdatesFrom(contact));
}
}
};
private Jid accountJid;
private Jid contactJid;
private boolean showDynamicTags = false;
@ -155,14 +177,18 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private void checkContactPermissionAndShowAddDialog() {
if (hasContactsPermission()) {
showAddToPhoneBookDialog();
} else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
} else if (QuickConversationsService.isContactListIntegration(this)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(
new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
}
}
private boolean hasContactsPermission() {
if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
if (QuickConversationsService.isContactListIntegration(this)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED;
} else {
return true;
}
@ -170,9 +196,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private void showAddToPhoneBookDialog() {
final Jid jid = contact.getJid();
final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy()
&& Config.QUICKSY_DOMAIN.equals(jid.getDomain())
&& jid.getLocal() != null;
final boolean quicksyContact =
AbstractQuickConversationsService.isQuicksy()
&& Config.QUICKSY_DOMAIN.equals(jid.getDomain())
&& jid.getLocal() != null;
final String value;
if (quicksyContact) {
value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid);
@ -183,24 +210,33 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
builder.setTitle(getString(R.string.action_add_phone_book));
builder.setMessage(getString(R.string.add_phone_book_text, value));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.setType(Contacts.CONTENT_ITEM_TYPE);
if (quicksyContact) {
intent.putExtra(Intents.Insert.PHONE, value);
} else {
intent.putExtra(Intents.Insert.IM_HANDLE, value);
intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
//TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP'
// however we dont have such a field and thus have to use the legacy PROTOCOL_JABBER
}
intent.putExtra("finishActivityOnSaveCompleted", true);
try {
startActivityForResult(intent, 0);
} catch (ActivityNotFoundException e) {
Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
}
});
builder.setPositiveButton(
getString(R.string.add),
(dialog, which) -> {
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.setType(Contacts.CONTENT_ITEM_TYPE);
if (quicksyContact) {
intent.putExtra(Intents.Insert.PHONE, value);
} else {
intent.putExtra(Intents.Insert.IM_HANDLE, value);
intent.putExtra(
Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
// TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a
// value of 'XMPP'
// however we dont have such a field and thus have to use the legacy
// PROTOCOL_JABBER
}
intent.putExtra("finishActivityOnSaveCompleted", true);
try {
startActivityForResult(intent, 0);
} catch (ActivityNotFoundException e) {
Toast.makeText(
ContactDetailsActivity.this,
R.string.no_application_found_to_view_contact,
Toast.LENGTH_SHORT)
.show();
}
});
builder.create().show();
}
@ -227,7 +263,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
protected String getShareableUri(boolean http) {
if (http) {
return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
return "https://conversations.im/i/"
+ XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
} else {
return "xmpp:" + Uri.encode(contact.getJid().asBareJid().toEscapedString(), "@/+");
}
@ -236,7 +273,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false);
showInactiveOmemo =
savedInstanceState != null
&& savedInstanceState.getBoolean("show_inactive_omemo", false);
if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
try {
this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT));
@ -253,10 +292,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
binding.showInactiveDevices.setOnClickListener(v -> {
showInactiveOmemo = !showInactiveOmemo;
populateView();
});
binding.showInactiveDevices.setOnClickListener(
v -> {
showInactiveOmemo = !showInactiveOmemo;
populateView();
});
binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
this.binding.media.setAdapter(mMediaAdapter);
@ -284,12 +324,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
this.showLastSeen = preferences.getBoolean("last_activity", false);
binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
binding.mediaWrapper.setVisibility(
Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
mMediaAdapter.setAttachments(Collections.emptyList());
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// TODO check for Camera / Scan permission
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0)
@ -334,15 +376,20 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setTitle(getString(R.string.action_delete_contact))
.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()))
.setPositiveButton(getString(R.string.delete),
removeFromRoster).create().show();
.setMessage(
JidDialog.style(
this,
R.string.remove_contact_text,
contact.getJid().toEscapedString()))
.setPositiveButton(getString(R.string.delete), removeFromRoster)
.create()
.show();
break;
case R.id.action_save:
saveEdits();
break;
case R.id.action_edit_contact:
Uri systemAccount = contact.getSystemAccount();
final Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) {
menuItem.expandActionView();
EditText text = menuItem.getActionView().findViewById(R.id.search_field);
@ -394,31 +441,43 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
Toast.makeText(
ContactDetailsActivity.this,
R.string.no_application_found_to_view_contact,
Toast.LENGTH_SHORT)
.show();
}
}
break;
case R.id.action_block:
case R.id.action_block, R.id.action_unblock:
BlockContactDialog.show(this, contact);
break;
case R.id.action_unblock:
BlockContactDialog.show(this, contact);
case R.id.action_custom_notifications:
configureCustomNotifications(contact);
break;
}
return super.onOptionsItemSelected(menuItem);
}
private void configureCustomNotifications(final Contact contact) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
final var shortcut = xmppConnectionService.getShortcutService().getShortcutInfo(contact);
configureCustomNotification(shortcut);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.contact_details, menu);
AccountUtils.showHideMenuItems(menu);
edit = menu.findItem(R.id.action_edit_contact);
save = menu.findItem(R.id.action_save);
MenuItem block = menu.findItem(R.id.action_block);
MenuItem unblock = menu.findItem(R.id.action_unblock);
MenuItem edit = menu.findItem(R.id.action_edit_contact);
MenuItem delete = menu.findItem(R.id.action_delete_contact);
final MenuItem block = menu.findItem(R.id.action_block);
final MenuItem unblock = menu.findItem(R.id.action_unblock);
final MenuItem edit = menu.findItem(R.id.action_edit_contact);
final MenuItem save = menu.findItem(R.id.action_save);
final MenuItem delete = menu.findItem(R.id.action_delete_contact);
final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
if (contact == null) {
return true;
}
@ -475,7 +534,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.statusMessage.setVisibility(View.VISIBLE);
final Spannable span = new SpannableString(message);
if (Emoticons.isOnlyEmoji(message)) {
span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(
new RelativeSizeSpan(2.0f),
0,
message.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.statusMessage.setText(span);
} else {
@ -498,14 +561,16 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.detailsSendPresence.setText(R.string.send_presence_updates);
} else {
binding.detailsSendPresence.setText(R.string.preemptively_grant);
binding.detailsSendPresence.setChecked(contact.getOption(Contact.Options.PREEMPTIVE_GRANT));
binding.detailsSendPresence.setChecked(
contact.getOption(Contact.Options.PREEMPTIVE_GRANT));
}
if (contact.getOption(Contact.Options.TO)) {
binding.detailsReceivePresence.setText(R.string.receive_presence_updates);
binding.detailsReceivePresence.setChecked(true);
} else {
binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates);
binding.detailsReceivePresence.setChecked(contact.getOption(Contact.Options.ASKING));
binding.detailsReceivePresence.setChecked(
contact.getOption(Contact.Options.ASKING));
}
if (contact.getAccount().isOnlineAndConnected()) {
binding.detailsReceivePresence.setEnabled(true);
@ -531,7 +596,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
&& contact.getLastseen() > 0
&& contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
binding.detailsLastseen.setVisibility(View.VISIBLE);
binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
binding.detailsLastseen.setText(
UIHelper.lastseen(
getApplicationContext(),
contact.isActive(),
contact.getLastseen()));
} else {
binding.detailsLastseen.setVisibility(View.GONE);
}
@ -540,7 +609,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
final String account = contact.getAccount().getJid().asBareJid().toEscapedString();
binding.detailsAccount.setText(getString(R.string.using_account, account));
AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
AvatarWorkerTask.loadAvatar(
contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
binding.detailsContactBadge.setOnClickListener(this::onBadgeClick);
binding.detailsContactBadge.setOnLongClickListener(v -> {
ShowAvatarPopup(ContactDetailsActivity.this, contact);
@ -551,7 +621,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
final LayoutInflater inflater = getLayoutInflater();
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
if (Config.supportOmemo() && axolotlService != null) {
final Collection<XmppAxolotlSession> sessions = axolotlService.findSessionsForContact(contact);
final Collection<XmppAxolotlSession> sessions =
axolotlService.findSessionsForContact(contact);
boolean anyActive = false;
for (XmppAxolotlSession session : sessions) {
anyActive = session.getTrust().isActive();
@ -581,9 +652,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
showUnverifiedWarning = true;
}
}
binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
binding.unverifiedWarning.setVisibility(
showUnverifiedWarning ? View.VISIBLE : View.GONE);
if (showsInactive || skippedInactive) {
binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
binding.showInactiveDevices.setText(
showsInactive
? R.string.hide_inactive_devices
: R.string.show_inactive_devices);
binding.showInactiveDevices.setVisibility(View.VISIBLE);
} else {
binding.showInactiveDevices.setVisibility(View.GONE);
@ -592,7 +667,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.showInactiveDevices.setVisibility(View.GONE);
}
final boolean isCameraFeatureAvailable = isCameraFeatureAvailable();
binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
binding.scanButton.setVisibility(
hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
if (hasKeys) {
binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
}
@ -603,7 +679,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
TextView keyType = view.findViewById(R.id.key_type);
keyType.setText(R.string.openpgp_key_id);
if ("pgp".equals(messageFingerprint)) {
keyType.setTextColor(MaterialColors.getColor(keyType, com.google.android.material.R.attr.colorPrimaryVariant));
keyType.setTextColor(
MaterialColors.getColor(
keyType, com.google.android.material.R.attr.colorPrimaryVariant));
}
key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
@ -615,7 +693,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
final List<ListItem.Tag> tagList = contact.getTags(this);
final boolean hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
final boolean hasMetaTags =
contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
binding.tags.setVisibility(View.GONE);
} else {
@ -624,9 +703,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
for (final ListItem.Tag tag : tagList) {
final String name = tag.getName();
final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
final TextView tv =
(TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
tv.setText(name);
tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
tv.setBackgroundTintList(
ColorStateList.valueOf(
MaterialColors.harmonizeWithPrimary(
this, XEP0392Helper.rgbFromNick(name))));
final int id = ViewCompat.generateViewId();
tv.setId(id);
viewIdBuilder.add(id);
@ -634,11 +717,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
if (contact.isBlocked()) {
final TextView tv =
(TextView)
inflater.inflate(
R.layout.list_item_tag, binding.tags, false);
(TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
tv.setText(R.string.blocked);
tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(), ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
tv.setBackgroundTintList(
ColorStateList.valueOf(
MaterialColors.harmonizeWithPrimary(
tv.getContext(),
ContextCompat.getColor(
tv.getContext(), R.color.gray_800))));
final int id = ViewCompat.generateViewId();
tv.setId(id);
viewIdBuilder.add(id);
@ -647,9 +733,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
final Presence.Status status = contact.getShownStatus();
if (status != Presence.Status.OFFLINE) {
final TextView tv =
(TextView)
inflater.inflate(
R.layout.list_item_tag, binding.tags, false);
(TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
UIHelper.setStatus(tv, status);
final int id = ViewCompat.generateViewId();
tv.setId(id);
@ -737,8 +821,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
if (Compatibility.hasStoragePermission(this)) {
final int limit = GridManager.getCurrentColumnCount(this.binding.media);
xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
xmppConnectionService.getAttachments(
account, contact.getJid().asBareJid(), limit, this);
this.binding.showMedia.setOnClickListener(
(v) -> MediaBrowserActivity.launch(this, contact));
}
final VcardAdapter items = new VcardAdapter();
@ -816,7 +902,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
if (contact != null
&& contact.getJid().asBareJid().equals(uri.getJid())
&& uri.hasFingerprints()) {
if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
}
@ -827,12 +915,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
@Override
public void onMediaLoaded(List<Attachment> attachments) {
runOnUiThread(() -> {
int limit = GridManager.getCurrentColumnCount(binding.media);
mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
});
runOnUiThread(
() -> {
int limit = GridManager.getCurrentColumnCount(binding.media);
mMediaAdapter.setAttachments(
attachments.subList(0, Math.min(limit, attachments.size())));
binding.mediaWrapper.setVisibility(
attachments.size() > 0 ? View.VISIBLE : View.GONE);
});
}
class VcardAdapter extends ArrayAdapter<Element> {

View file

@ -238,11 +238,14 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import im.conversations.android.xmpp.model.stanza.Iq;
import me.drakeet.support.toast.ToastCompat;
import org.jetbrains.annotations.NotNull;
public class ConversationFragment extends XmppFragment
implements EditMessage.KeyboardListener,
MessageAdapter.OnContactPictureLongClicked,
@ -564,7 +567,10 @@ public class ConversationFragment extends XmppFragment
} catch (IllegalStateException e) {
Log.d(
Config.LOGTAG,
"caught illegal state exception while updating status messages");
"caught illegal state"
+ " exception while"
+ " updating status"
+ " messages");
}
messageListAdapter
.notifyDataSetChanged();
@ -1008,14 +1014,6 @@ public class ConversationFragment extends XmppFragment
for (int i = 0; i < messages.size(); ++i) {
if (uuid.equals(messages.get(i).getUuid())) {
return i;
} else {
Message next = messages.get(i);
while (next != null && next.wasMergedIntoPrevious(activity == null ? null : activity.xmppConnectionService)) {
if (uuid.equals(next.getUuid())) {
return i;
}
next = next.next();
}
}
}
return -1;
@ -1683,13 +1681,12 @@ public class ConversationFragment extends XmppFragment
menuCall.setVisible(false);
} else {
menuOngoingCall.setVisible(false);
final RtpCapability.Capability rtpCapability =
RtpCapability.check(conversation.getContact());
// use RtpCapability.check(conversation.getContact()); to check if contact
// actually has support
final boolean cameraAvailable =
activity != null && activity.isCameraFeatureAvailable();
menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE);
menuVideoCall.setVisible(
rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable);
menuCall.setVisible(true);
menuVideoCall.setVisible(cameraAvailable);
}
menuContactDetails.setVisible(!this.conversation.withSelf());
menuMucDetails.setVisible(false);
@ -2170,13 +2167,9 @@ public class ConversationFragment extends XmppFragment
}
}
private void populateContextMenu(ContextMenu menu) {
private void populateContextMenu(final ContextMenu menu) {
final Message m = this.selectedMessage;
final Transferable t = m.getTransferable();
Message relevantForCorrection = m;
while (relevantForCorrection.mergeable(relevantForCorrection.next())) {
relevantForCorrection = relevantForCorrection.next();
}
if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) {
if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
@ -2214,7 +2207,8 @@ public class ConversationFragment extends XmppFragment
final MenuItem sendAgain = menu.findItem(R.id.send_again);
final MenuItem copyUrl = menu.findItem(R.id.copy_url);
final MenuItem saveToDownloads = menu.findItem(R.id.save_to_downloads);
final MenuItem saveAsSticker = menu.findItem(R.id.save_as_sticker);
final MenuItem copyLink = menu.findItem(R.id.copy_link);
MenuItem saveAsSticker = menu.findItem(R.id.save_as_sticker);
final MenuItem downloadFile = menu.findItem(R.id.download_file);
final MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
final MenuItem blockMedia = menu.findItem(R.id.block_media);
@ -2227,7 +2221,8 @@ public class ConversationFragment extends XmppFragment
&& m.getErrorMessage() != null
&& !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage());
final Conversational conversational = m.getConversation();
if (m.getStatus() == Message.STATUS_RECEIVED && conversational instanceof Conversation c) {
if (m.getStatus() == Message.STATUS_RECEIVED
&& conversational instanceof Conversation c) {
final XmppConnection connection = c.getAccount().getXmppConnection();
if (c.isWithStranger()
&& m.getServerMsgId() != null
@ -2237,7 +2232,6 @@ public class ConversationFragment extends XmppFragment
reportAndBlock.setVisible(true);
}
}
// addReaction.setVisible(!showError && !m.isDeleted());
if (!m.isFileOrImage()
&& !encrypted
&& !m.isGeoUri()
@ -2245,20 +2239,29 @@ public class ConversationFragment extends XmppFragment
&& !unInitiatedButKnownSize
&& 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) {
copyLink.setVisible(true);
}
}
quoteMessage.setVisible(!encrypted && !showError);
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) {
retryDecryption.setVisible(true);
}
if (!showError
&& relevantForCorrection.getType() == Message.TYPE_TEXT
&& relevantForCorrection.isEditable()
&& m.getType() == Message.TYPE_TEXT
&& m.isEditable()
&& !m.isGeoUri()
&& m.getConversation() instanceof Conversation) {
correctMessage.setVisible(true);
if (!relevantForCorrection.getBody().equals("") && !relevantForCorrection.getBody().equals(" ")) retractMessage.setVisible(true);
if (!m.getBody().equals("") && !m.getBody().equals(" ")) retractMessage.setVisible(true);
}
if (relevantForCorrection.getStatus() == Message.STATUS_WAITING) {
if (m.getStatus() == Message.STATUS_WAITING) {
correctMessage.setVisible(true);
retractMessage.setVisible(true);
}
@ -2343,10 +2346,8 @@ public class ConversationFragment extends XmppFragment
.setTitle(R.string.retract_message)
.setMessage(R.string.do_you_really_want_to_retract_this_message)
.setPositiveButton(R.string.yes, (dialog, whichButton) -> {
Message message = selectedMessage;
while (message.mergeable(message.next())) {
message = message.next();
}
final var message = selectedMessage;
if (message.getStatus() == Message.STATUS_WAITING || message.getStatus() == Message.STATUS_OFFERED) {
activity.xmppConnectionService.deleteMessage(message);
return;
@ -2380,11 +2381,7 @@ public class ConversationFragment extends XmppFragment
return true;
case R.id.moderate_message:
activity.quickEdit("Spam", (reason) -> {
Message message = selectedMessage;
do {
activity.xmppConnectionService.moderateMessage(conversation.getAccount(), message, reason);
message = message.mergeable(message.next()) ? message.next() : null;
} while (message != null);
activity.xmppConnectionService.moderateMessage(conversation.getAccount(), selectedMessage, reason);
return null;
}, R.string.moderate_reason, false, false, true, true);
return true;
@ -2722,9 +2719,9 @@ public class ConversationFragment extends XmppFragment
private void addShortcut() {
ShortcutInfoCompat info;
if (conversation.getMode() == Conversation.MODE_MULTI) {
info = activity.xmppConnectionService.getShortcutService().getShortcutInfoCompat(conversation.getMucOptions());
info = activity.xmppConnectionService.getShortcutService().getShortcutInfo(conversation.getMucOptions());
} else {
info = activity.xmppConnectionService.getShortcutService().getShortcutInfoCompat(conversation.getContact());
info = activity.xmppConnectionService.getShortcutService().getShortcutInfo(conversation.getContact());
}
ShortcutManagerCompat.requestPinShortcut(activity, info, null);
}
@ -2808,7 +2805,11 @@ public class ConversationFragment extends XmppFragment
}
private void triggerRtpSession(final Account account, final Jid with, final String action) {
CallIntegrationConnectionService.placeCall(activity.xmppConnectionService, account,with,RtpSessionActivity.actionToMedia(action));
CallIntegrationConnectionService.placeCall(
activity.xmppConnectionService,
account,
with,
RtpSessionActivity.actionToMedia(action));
}
private void handleAttachmentSelection(MenuItem item) {
@ -2894,10 +2895,14 @@ public class ConversationFragment extends XmppFragment
}
public void attachFile(final int attachmentChoice) {
attachFile(attachmentChoice, true);
attachFile(attachmentChoice, true, false);
}
public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed) {
attachFile(attachmentChoice, updateRecentlyUsed, false);
}
public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed, final boolean fromPermissions) {
if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
if (!hasPermissions(
attachmentChoice,
@ -2906,7 +2911,8 @@ public class ConversationFragment extends XmppFragment
return;
}
} else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO
|| attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) {
|| attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO
|| (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE && !fromPermissions)) {
if (!hasPermissions(
attachmentChoice,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
@ -2997,7 +3003,7 @@ public class ConversationFragment extends XmppFragment
final PermissionUtils.PermissionResult permissionResult =
PermissionUtils.removeBluetoothConnect(permissions, grantResults);
if (grantResults.length > 0) {
if (allGranted(permissionResult.grantResults)) {
if (allGranted(permissionResult.grantResults) || requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
switch (requestCode) {
case REQUEST_START_DOWNLOAD:
if (this.mPendingDownloadableMessage != null) {
@ -3019,7 +3025,7 @@ public class ConversationFragment extends XmppFragment
triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
break;
default:
attachFile(requestCode);
attachFile(requestCode, true, true);
break;
}
} else {
@ -3124,7 +3130,8 @@ public class ConversationFragment extends XmppFragment
@SuppressLint("InflateParams")
protected void clearHistoryDialog(final Conversation conversation) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireActivity());
builder.setTitle(R.string.clear_conversation_history);
final View dialogView =
requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
@ -3148,7 +3155,8 @@ public class ConversationFragment extends XmppFragment
}
protected void muteConversationDialog(final Conversation conversation) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireActivity());
builder.setTitle(R.string.disable_notifications);
final int[] durations = activity.getResources().getIntArray(R.array.mute_options_durations);
final CharSequence[] labels = new CharSequence[durations.length];
@ -3180,7 +3188,9 @@ public class ConversationFragment extends XmppFragment
private boolean hasPermissions(int requestCode, List<String> permissions) {
final List<String> missingPermissions = new ArrayList<>();
for (String permission : permissions) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|| Config.ONLY_INTERNAL_STORAGE)
&& permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
continue;
}
if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
@ -3190,9 +3200,7 @@ public class ConversationFragment extends XmppFragment
if (missingPermissions.size() == 0) {
return true;
} else {
requestPermissions(
missingPermissions.toArray(new String[0]),
requestCode);
requestPermissions(missingPermissions.toArray(new String[0]), requestCode);
return false;
}
}
@ -3342,9 +3350,6 @@ public class ConversationFragment extends XmppFragment
}
}
if (message != null) {
while (message.next() != null && message.next().wasMergedIntoPrevious(activity == null ? null : activity.xmppConnectionService)) {
message = message.next();
}
return message;
}
}
@ -3378,7 +3383,15 @@ public class ConversationFragment extends XmppFragment
}
private void addReaction(final Message message) {
activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message, reactions));
activity.addReaction(
message,
reactions -> {
if (activity.xmppConnectionService.sendReactions(message, reactions)) {
return;
}
Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG)
.show();
});
}
private void reportMessage(final Message message) {
@ -3386,7 +3399,8 @@ public class ConversationFragment extends XmppFragment
}
private void showErrorMessage(final Message message) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireActivity());
builder.setTitle(R.string.error_message);
final String errorMessage = message.getErrorMessage();
final String[] errorMessageParts =
@ -3457,7 +3471,8 @@ public class ConversationFragment extends XmppFragment
}
private void deleteFile(final Message message) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireActivity());
builder.setNegativeButton(R.string.cancel, null);
builder.setTitle(R.string.delete_file_dialog);
builder.setMessage(R.string.delete_file_dialog_msg);
@ -3585,10 +3600,7 @@ public class ConversationFragment extends XmppFragment
updateEditablity();
}
private void correctMessage(Message message) {
while (message.mergeable(message.next())) {
message = message.next();
}
private void correctMessage(final Message message) {
if (activity.xmppConnectionService != null && activity.xmppConnectionService.getBooleanPreference("show_thread_feature", R.bool.show_thread_feature)) {
setThread(message.getThread());
}
@ -3711,7 +3723,8 @@ public class ConversationFragment extends XmppFragment
final String uuid = pendingConversationsUuid.pop();
Log.d(
Config.LOGTAG,
"ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid="
"ConversationFragment.onStart() - activity was bound but no conversation"
+ " loaded. uuid="
+ uuid);
if (uuid != null) {
findAndReInitByUuidOrArchive(uuid);
@ -4266,7 +4279,10 @@ public class ConversationFragment extends XmppFragment
R.string.enable,
this.mEnableAccountListener);
} else if (account.getStatus() == Account.State.LOGGED_OUT) {
showSnackbar(R.string.this_account_is_logged_out,R.string.log_in,this.mEnableAccountListener);
showSnackbar(
R.string.this_account_is_logged_out,
R.string.log_in,
this.mEnableAccountListener);
} else if (conversation.isBlocked()) {
showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
} else if (account.getStatus() == Account.State.CONNECTING) {
@ -4332,7 +4348,8 @@ public class ConversationFragment extends XmppFragment
showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
break;
case TECHNICAL_PROBLEMS:
showSnackbar(R.string.conference_technical_problems, R.string.try_again, joinMuc);
showSnackbar(
R.string.conference_technical_problems, R.string.try_again, joinMuc);
break;
case UNKNOWN:
showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc);
@ -4915,8 +4932,10 @@ public class ConversationFragment extends XmppFragment
});
}
public void showNoPGPKeyDialog(final boolean plural, final DialogInterface.OnClickListener listener) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
public void showNoPGPKeyDialog(
final boolean plural, final DialogInterface.OnClickListener listener) {
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireActivity());
if (plural) {
builder.setTitle(getString(R.string.no_pgp_keys));
builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
@ -5085,7 +5104,13 @@ public class ConversationFragment extends XmppFragment
try {
getActivity()
.startIntentSenderForResult(
pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
pendingIntent.getIntentSender(),
requestCode,
null,
0,
0,
0,
Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}

View file

@ -60,7 +60,6 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@ -137,8 +136,22 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import me.drakeet.support.toast.ToastCompat;
import p32929.easypasscodelock.Utils.EasyLock;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.openintents.openpgp.util.OpenPgpApi;
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
public class ConversationsActivity extends XmppActivity
implements OnConversationSelected,
OnConversationArchived,
OnConversationsListItemUpdated,
OnConversationRead,
XmppConnectionService.OnAccountUpdate,
XmppConnectionService.OnConversationUpdate,
XmppConnectionService.OnRosterUpdate,
OnUpdateBlocklist,
XmppConnectionService.OnShowErrorToast,
XmppConnectionService.OnAffiliationChanged {
public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
public static final String EXTRA_CONVERSATION = "conversationUuid";
@ -154,11 +167,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public static final String EXTRA_NODE = "node";
public static final String EXTRA_JID = "jid";
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
ACTION_VIEW_CONVERSATION,
Intent.ACTION_SEND,
Intent.ACTION_SEND_MULTIPLE
);
private static final List<String> VIEW_AND_SHARE_ACTIONS =
Arrays.asList(
ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE);
public static final int REQUEST_OPEN_MESSAGE = 0x9876;
public static final int REQUEST_PLAY_PAUSE = 0x5432;
@ -182,9 +193,11 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public static final long DRAWER_START_CHAT_PUBLIC = 14;
public static final long DRAWER_START_CHAT_DISCOVER = 15;
//secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment
private static final @IdRes
int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment};
// secondary fragment (when holding the conversation, must be initialized before refreshing the
// overview fragment
private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = {
R.id.secondary_fragment, R.id.main_fragment
};
private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
private ActivityConversationsBinding binding;
@ -202,7 +215,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
private static boolean isViewOrShareIntent(Intent i) {
Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction()));
return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION);
return i != null
&& VIEW_AND_SHARE_ACTIONS.contains(i.getAction())
&& i.hasExtra(EXTRA_CONVERSATION);
}
private static Intent createLauncherIntent(Context context) {
@ -454,7 +469,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
invalidateActionBarTitle();
if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) {
if (binding.secondaryFragment != null
&& ConversationFragment.getConversation(this) == null) {
Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
if (conversation != null) {
openConversation(conversation, null);
@ -722,7 +738,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
return performRedirectIfNecessary(null, noAnimation);
}
private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) {
private boolean performRedirectIfNecessary(
final Conversation ignore, final boolean noAnimation) {
if (xmppConnectionService == null) {
return false;
}
@ -733,12 +750,13 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (noAnimation) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
}
runOnUiThread(() -> {
startActivity(intent);
if (noAnimation) {
overridePendingTransition(0, 0);
}
});
runOnUiThread(
() -> {
startActivity(intent);
if (noAnimation) {
overridePendingTransition(0, 0);
}
});
}
return mRedirectInProcess.get();
}
@ -764,7 +782,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private String getBatteryOptimizationPreferenceKey() {
@SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
@SuppressLint("HardwareIds")
String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
return "show_battery_optimization" + (device == null ? "" : device);
}
@ -773,20 +792,31 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private boolean openBatteryOptimizationDialogIfNeeded() {
if (isOptimizingBattery() && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
if (isOptimizingBattery()
&& getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.battery_optimizations_enabled);
builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name)));
builder.setPositiveButton(R.string.next, (dialog, which) -> {
final Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
final Uri uri = Uri.parse("package:" + getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (final ActivityNotFoundException e) {
Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
}
});
builder.setMessage(
getString(
R.string.battery_optimizations_enabled_dialog,
getString(R.string.app_name)));
builder.setPositiveButton(
R.string.next,
(dialog, which) -> {
final Intent intent =
new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
final Uri uri = Uri.parse("package:" + getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (final ActivityNotFoundException e) {
Toast.makeText(
this,
R.string.device_does_not_support_battery_op,
Toast.LENGTH_SHORT)
.show();
}
});
builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
@ -797,8 +827,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
private boolean requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(
new String[] {Manifest.permission.POST_NOTIFICATIONS},
REQUEST_POST_NOTIFICATION);
return true;
}
return false;
@ -904,9 +938,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
}
private boolean processViewIntent(Intent intent) {
private boolean processViewIntent(final Intent intent) {
final String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
final Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null;
final Conversation conversation =
uuid != null ? xmppConnectionService.findConversationByUuidReliable(uuid) : null;
if (conversation == null) {
Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
return false;
@ -916,7 +951,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
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) {
@ -1122,7 +1158,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public void onConversationSelected(Conversation conversation) {
clearPendingViewIntent();
if (ConversationFragment.getConversation(this) == conversation) {
Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open");
Log.d(
Config.LOGTAG,
"ignore onConversationSelected() because conversation is already open");
return;
}
openConversation(conversation, null);
@ -1155,13 +1193,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
private void displayToast(final String msg) {
runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
runOnUiThread(
() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
}
@Override
public void onAffiliationChangedSuccessful(Jid jid) {
}
public void onAffiliationChangedSuccessful(Jid jid) {}
@Override
public void onAffiliationChangeFailed(Jid jid, int resId) {
@ -1171,7 +1208,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
private void openConversation(Conversation conversation, Bundle extras) {
final FragmentManager fragmentManager = getFragmentManager();
executePendingTransactions(fragmentManager);
ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
ConversationFragment conversationFragment =
(ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
final boolean mainNeedsRefresh;
if (conversationFragment == null) {
mainNeedsRefresh = false;
@ -1187,7 +1225,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
fragmentTransaction.commit();
} catch (IllegalStateException e) {
Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
//allowing state loss is probably fine since view intents et all are already stored and a click can probably be 'ignored'
// allowing state loss is probably fine since view intents et all are already
// stored and a click can probably be 'ignored'
return;
}
}
@ -1205,14 +1244,15 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
try {
fragmentManager.executePendingTransactions();
} catch (final Exception e) {
Log.e(Config.LOGTAG,"unable to execute pending fragment transactions");
Log.e(Config.LOGTAG, "unable to execute pending fragment transactions");
}
}
public boolean onXmppUriClicked(Uri uri) {
XmppUri xmppUri = new XmppUri(uri);
if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri);
final Conversation conversation =
xmppConnectionService.findUniqueConversationByJid(xmppUri);
if (conversation != null) {
if (xmppUri.getParameter("password") != null) {
xmppConnectionService.providePasswordForMuc(conversation, xmppUri.getParameter("password"));
@ -1386,7 +1426,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
final FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
final Fragment secondaryFragment =
fragmentManager.findFragmentById(R.id.secondary_fragment);
if (mainFragment != null) {
if (binding.secondaryFragment != null) {
if (mainFragment instanceof ConversationFragment) {
@ -1503,7 +1544,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
binding.toolbarSubtitle.setVisibility(View.GONE);
binding.toolbarAvatar.setVisibility(View.GONE);
}
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
final Fragment secondaryFragment =
fragmentManager.findFragmentById(R.id.secondary_fragment);
if (secondaryFragment instanceof ConversationFragment conversationFragment) {
final Conversation conversation = conversationFragment.getConversation();
if (conversation != null) {
@ -1658,15 +1700,21 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
try {
fragmentManager.popBackStack();
} catch (final IllegalStateException e) {
Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e);
//this usually means activity is no longer active; meaning on the next open we will run through this again
Log.w(
Config.LOGTAG,
"state loss while popping back state after archiving conversation",
e);
// this usually means activity is no longer active; meaning on the next open we will
// run through this again
}
return;
}
final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
final Fragment secondaryFragment =
fragmentManager.findFragmentById(R.id.secondary_fragment);
if (secondaryFragment instanceof ConversationFragment) {
if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
Conversation suggestion =
ConversationsOverviewFragment.getSuggestion(this, conversation);
if (suggestion != null) {
openConversation(suggestion, null);
}

View file

@ -765,7 +765,7 @@ public class EditAccountActivity extends OmemoActivity
this.binding.hostname.addTextChangedListener(mTextWatcher);
this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener);
this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog());
this.binding.port.setText(String.valueOf(Resolver.DEFAULT_PORT_XMPP));
this.binding.port.setText(String.valueOf(Resolver.XMPP_PORT_STARTTLS));
this.binding.port.addTextChangedListener(mTextWatcher);
this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener);
this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener);
@ -1111,11 +1111,7 @@ public class EditAccountActivity extends OmemoActivity
}
private void deleteAccount() {
this.deleteAccount(
mAccount,
() -> {
finish();
});
this.deleteAccount(mAccount, () -> finish());
}
private boolean inNeedOfSaslAccept() {
@ -1524,7 +1520,7 @@ public class EditAccountActivity extends OmemoActivity
if (hasKeys
&& Config.supportOmemo()) { // TODO: either the button should be visible if we
// print an active device or the device list should
// be fed with reactived devices
// be fed with reactivated devices
this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty()) {
@ -1553,12 +1549,17 @@ public class EditAccountActivity extends OmemoActivity
}
} else {
final TextInputLayout errorLayout;
if (this.mAccount.errorStatus()) {
if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED
|| this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) {
final var status = this.mAccount.getStatus();
if (status.isError()
|| Arrays.asList(
Account.State.NO_INTERNET,
Account.State.MISSING_INTERNET_PERMISSION)
.contains(status)) {
if (status == Account.State.UNAUTHORIZED
|| status == Account.State.DOWNGRADE_ATTACK) {
errorLayout = this.binding.accountPasswordLayout;
} else if (mShowOptions
&& this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND
&& status == Account.State.SERVER_NOT_FOUND
&& this.binding.hostname.getText().length() > 0) {
errorLayout = this.binding.hostnameLayout;
} else {

View file

@ -29,6 +29,8 @@
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;
@ -40,10 +42,9 @@ import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.databinding.DataBindingUtil;
import com.canhub.cropper.CropImage;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
@ -52,20 +53,15 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.ui.util.PendingItem;
import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE;
import com.canhub.cropper.CropImage;
public class PublishGroupChatProfilePictureActivity extends XmppActivity implements OnAvatarPublication {
public class PublishGroupChatProfilePictureActivity extends XmppActivity
implements OnAvatarPublication {
private final PendingItem<String> pendingConversationUuid = new PendingItem<>();
private ActivityPublishProfilePictureBinding binding;
private Conversation conversation;
private Uri uri;
@Override
protected void refreshUiReal() {
}
protected void refreshUiReal() {}
@Override
protected void onBackendConnected() {
@ -99,14 +95,16 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
this.binding =
DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture);
this.binding.contactOnly.setVisibility(View.GONE);
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
setSupportActionBar(this.binding.toolbar);
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) -> PublishProfilePictureActivity.chooseAvatar(this));
final var intent = getIntent();
final var uuid = intent == null ? null : intent.getStringExtra("uuid");
if (uuid != null) {
@ -116,7 +114,6 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme
this.binding.publishButton.setOnClickListener(this::publish);
}
private void publish(final View view) {
binding.publishButton.setText(R.string.publishing);
binding.publishButton.setEnabled(false);
@ -163,18 +160,21 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme
@Override
public void onAvatarPublicationSucceeded() {
runOnUiThread(() -> {
Toast.makeText(this, R.string.avatar_has_been_published, Toast.LENGTH_SHORT).show();
finish();
});
runOnUiThread(
() -> {
Toast.makeText(this, R.string.avatar_has_been_published, Toast.LENGTH_SHORT)
.show();
finish();
});
}
@Override
public void onAvatarPublicationFailed(@StringRes int res) {
runOnUiThread(() -> {
Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
this.binding.publishButton.setText(R.string.publish);
this.binding.publishButton.setEnabled(true);
});
runOnUiThread(
() -> {
Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
this.binding.publishButton.setText(R.string.publish);
this.binding.publishButton.setEnabled(true);
});
}
}

View file

@ -180,7 +180,6 @@ public class PublishProfilePictureActivity extends XmppActivity
super.onSaveInstanceState(outState);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -199,7 +198,7 @@ public class PublishProfilePictureActivity extends XmppActivity
}
} else if (requestCode == REQUEST_CHOOSE_PICTURE) {
if (resultCode == RESULT_OK) {
cropUri(data.getData());
cropUri(this, data.getData());
}
}
}
@ -254,7 +253,7 @@ public class PublishProfilePictureActivity extends XmppActivity
final Uri uri = intent != null ? intent.getData() : null;
if (uri != null && handledExternalUri.compareAndSet(false, true)) {
cropUri(uri);
cropUri(this, uri);
return;
}
@ -265,16 +264,17 @@ public class PublishProfilePictureActivity extends XmppActivity
getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get());
}
public void cropUri(final Uri uri) {
public void cropUri(final Activity activity, final Uri uri) {
if (Build.VERSION.SDK_INT >= 28) {
loadImageIntoPreview(uri);
if (this.binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || this.binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) {
if (binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) {
this.avatarUri = uri;
return;
}
}
CropImage.activity(uri).setOutputCompressFormat(Bitmap.CompressFormat.PNG)
CropImage.activity(uri)
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
.setAspectRatio(1, 1)
.setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE)
.start(this);
@ -284,10 +284,21 @@ public class PublishProfilePictureActivity extends XmppActivity
Drawable bm = null;
if (uri == null) {
bm = avatarService().get(account, (int) getResources().getDimension(R.dimen.publish_avatar_size));
bm =
avatarService()
.get(
account,
(int) getResources().getDimension(R.dimen.publish_avatar_size));
} else {
try {
bm = xmppConnectionService.getFileBackend().cropCenterSquareDrawable(uri, (int) getResources().getDimension(R.dimen.publish_avatar_size));
bm =
xmppConnectionService
.getFileBackend()
.cropCenterSquareDrawable(
uri,
(int)
getResources()
.getDimension(R.dimen.publish_avatar_size));
} catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to load bitmap into image view", e);
}
@ -340,5 +351,4 @@ public class PublishProfilePictureActivity extends XmppActivity
public void onAccountUpdate() {
refreshUi();
}
}

View file

@ -583,15 +583,17 @@ public class RtpSessionActivity extends XmppActivity
final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
final RtpEndUserState state =
extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
final var contact = account.getRoster().getContact(with);
if (state != null) {
Log.d(Config.LOGTAG, "restored last state from intent extra");
updateButtonConfiguration(state);
updateVerifiedShield(false);
updateStateDisplay(state);
updateIncomingCallScreen(state);
updateSupportWarning(state, contact);
invalidateOptionsMenu();
}
setWith(account.getRoster().getContact(with), state);
setWith(state, contact);
if (xmppConnectionService
.getJingleConnectionManager()
.fireJingleRtpConnectionStateUpdates()) {
@ -614,10 +616,10 @@ public class RtpSessionActivity extends XmppActivity
}
private void setWith(final RtpEndUserState state) {
setWith(getWith(), state);
setWith(state, getWith());
}
private void setWith(final Contact contact, final RtpEndUserState state) {
private void setWith(final RtpEndUserState state, final Contact contact) {
binding.with.setText(contact.getDisplayName());
if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL)
.contains(state)) {
@ -873,7 +875,9 @@ public class RtpSessionActivity extends XmppActivity
updateCallDuration();
updateVerifiedShield(false);
invalidateOptionsMenu();
setWith(account.getRoster().getContact(with), state);
final var contact = account.getRoster().getContact(with);
setWith(state, contact);
updateSupportWarning(state, contact);
}
private void reInitializeActivityWithRunningRtpSession(
@ -963,7 +967,7 @@ public class RtpSessionActivity extends XmppActivity
private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) {
if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
final boolean show = getResources().getBoolean(R.bool.is_portrait_mode);
if (show) {
binding.contactPhoto.setVisibility(View.VISIBLE);
if (contact == null) {
@ -988,6 +992,18 @@ public class RtpSessionActivity extends XmppActivity
}
}
private void updateSupportWarning(final RtpEndUserState state, final Contact contact) {
if (state == RtpEndUserState.CONNECTIVITY_ERROR
&& getResources().getBoolean(R.bool.is_portrait_mode)) {
binding.supportWarning.setVisibility(
RtpCapability.check(contact) == RtpCapability.Capability.NONE
? View.VISIBLE
: View.GONE);
} else {
binding.supportWarning.setVisibility(View.GONE);
}
}
private Set<Media> getMedia() {
return requireRtpConnection().getMedia();
}
@ -1581,6 +1597,7 @@ public class RtpSessionActivity extends XmppActivity
updateButtonConfiguration(state, media, contentAddition);
updateVideoViews(state);
updateIncomingCallScreen(state, contact);
updateSupportWarning(state, contact);
invalidateOptionsMenu();
});
if (END_CARD.contains(state)) {
@ -1658,6 +1675,7 @@ public class RtpSessionActivity extends XmppActivity
updateStateDisplay(state);
updateButtonConfiguration(state, media, null);
updateIncomingCallScreen(state);
updateSupportWarning(state, account.getRoster().getContact(with));
invalidateOptionsMenu();
});
resetIntent(account, with, state, media);

View file

@ -9,11 +9,11 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.common.collect.Iterables;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityShareWithBinding;
@ -125,7 +125,34 @@ public class ShareWithActivity extends XmppActivity
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
binding.chooseConversationList.setAdapter(mAdapter);
mAdapter.setConversationClickListener((view, conversation) -> share(conversation));
final var intent = getIntent();
final var shortcutId = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
this.share = new Share();
if (shortcutId != null) {
final var conversation = shortcutIdToConversation(shortcutId);
if (conversation != null) {
// we have everything we need. Jump into chat
populateShare(intent);
share(conversation);
}
}
}
private String shortcutIdToConversation(final String shortcutId) {
final var shortcut =
Iterables.tryFind(
ShortcutManagerCompat.getDynamicShortcuts(this),
si -> si.getId().equals(shortcutId));
if (shortcut.isPresent()) {
final var extras = shortcut.get().getExtras();
if (extras == null) {
return null;
} else {
return extras.getString(ConversationsActivity.EXTRA_CONVERSATION);
}
} else {
return null;
}
}
@Override
@ -150,10 +177,18 @@ public class ShareWithActivity extends XmppActivity
@Override
public void onStart() {
super.onStart();
Intent intent = getIntent();
final Intent intent = getIntent();
if (intent == null) {
return;
}
populateShare(intent);
if (xmppConnectionServiceBound) {
xmppConnectionService.populateWithOrderedConversations(
mConversations, this.share.uris.isEmpty(), false);
}
}
private void populateShare(final Intent intent) {
final String type = intent.getType();
final String action = intent.getAction();
final Uri data = intent.getData();
@ -230,8 +265,12 @@ public class ShareWithActivity extends XmppActivity
mPendingConversation = conversation;
return;
}
share(conversation.getUuid());
}
private void share(final String conversation) {
final Intent intent = new Intent(this, ConversationsActivity.class);
intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation);
if (!share.uris.isEmpty()) {
intent.setAction(Intent.ACTION_SEND_MULTIPLE);
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
@ -246,7 +285,7 @@ public class ShareWithActivity extends XmppActivity
}
try {
startActivity(intent);
} catch (SecurityException e) {
} catch (final SecurityException e) {
Toast.makeText(
this,
R.string.sharing_application_not_grant_permission,

View file

@ -8,6 +8,7 @@ import android.telephony.TelephonyManager;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
@ -38,6 +39,7 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.text.Html;
import android.text.InputType;
import android.util.DisplayMetrics;
@ -55,15 +57,16 @@ import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.Toast;
import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.base.Strings;
@ -97,6 +100,7 @@ import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.Reaction;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.NotificationService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
@ -107,16 +111,15 @@ import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Consumer;
@ -142,52 +145,60 @@ public abstract class XmppActivity extends ActionBarActivity {
protected boolean mUsingEnterKey = false;
protected boolean mUseTor = false;
protected Toast mToast;
public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
public Runnable onOpenPGPKeyPublished =
() ->
Toast.makeText(
XmppActivity.this,
R.string.openpgp_has_been_published,
Toast.LENGTH_SHORT)
.show();
protected ConferenceInvite mPendingConferenceInvite = null;
protected PriorityQueue<Pair<Integer, ValueCallback<Uri[]>>> activityCallbacks =
Build.VERSION.SDK_INT >= 24 ? new PriorityQueue<>((x, y) -> y.first.compareTo(x.first)) : new PriorityQueue<>();
protected ServiceConnection mConnection = new ServiceConnection() {
protected ServiceConnection mConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
xmppConnectionServiceBound = true;
registerListeners();
onBackendConnected();
}
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
xmppConnectionServiceBound = true;
registerListeners();
onBackendConnected();
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
xmppConnectionServiceBound = false;
}
};
@Override
public void onServiceDisconnected(ComponentName arg0) {
xmppConnectionServiceBound = false;
}
};
private DisplayMetrics metrics;
private long mLastUiRefresh = 0;
private final Handler mRefreshUiHandler = new Handler();
private final Runnable mRefreshUiRunnable = () -> {
mLastUiRefresh = SystemClock.elapsedRealtime();
refreshUiReal();
};
private final UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
@Override
public void success(final Conversation conversation) {
runOnUiThread(() -> {
switchToConversation(conversation);
hideToast();
});
}
private final Runnable mRefreshUiRunnable =
() -> {
mLastUiRefresh = SystemClock.elapsedRealtime();
refreshUiReal();
};
private final UiCallback<Conversation> adhocCallback =
new UiCallback<Conversation>() {
@Override
public void success(final Conversation conversation) {
runOnUiThread(
() -> {
switchToConversation(conversation);
hideToast();
});
}
@Override
public void error(final int errorCode, Conversation object) {
runOnUiThread(() -> replaceToast(getString(errorCode)));
}
@Override
public void error(final int errorCode, Conversation object) {
runOnUiThread(() -> replaceToast(getString(errorCode)));
}
@Override
public void userInputRequired(PendingIntent pi, Conversation object) {
}
};
@Override
public void userInputRequired(PendingIntent pi, Conversation object) {}
};
public static boolean cancelPotentialWork(Message message, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
@ -244,7 +255,7 @@ public abstract class XmppActivity extends ActionBarActivity {
}
}
abstract protected void refreshUiReal();
protected abstract void refreshUiReal();
@Override
public void onStart() {
@ -283,6 +294,31 @@ public abstract class XmppActivity extends ActionBarActivity {
}
}
@RequiresApi(api = Build.VERSION_CODES.R)
protected void configureCustomNotification(final ShortcutInfoCompat shortcut) {
final var notificationManager = getSystemService(NotificationManager.class);
final var channel =
notificationManager.getNotificationChannel(
NotificationService.MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
if (channel != null && channel.getConversationId() != null) {
ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
openNotificationSettings(shortcut);
} else {
NotificationService.createConversationChannel(this, shortcut);
ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
openNotificationSettings(shortcut);
}
}
@RequiresApi(api = Build.VERSION_CODES.R)
protected void openNotificationSettings(final ShortcutInfoCompat shortcut) {
final var intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(
Settings.EXTRA_CHANNEL_ID, NotificationService.MESSAGES_NOTIFICATION_CHANNEL);
intent.putExtra(Settings.EXTRA_CONVERSATION_ID, shortcut.getId());
startActivity(intent);
}
public boolean hasPgp() {
return xmppConnectionService.getPgpEngine() != null;
@ -292,16 +328,20 @@ public abstract class XmppActivity extends ActionBarActivity {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(getString(R.string.openkeychain_required));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name))));
builder.setMessage(
Html.fromHtml(
getString(
R.string.openkeychain_required_long,
getString(R.string.app_name))));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setNeutralButton(getString(R.string.restart),
builder.setNeutralButton(
getString(R.string.restart),
(dialog, which) -> {
if (xmppConnectionServiceBound) {
unbindService(mConnection);
xmppConnectionServiceBound = false;
}
stopService(new Intent(XmppActivity.this,
XmppConnectionService.class));
stopService(new Intent(XmppActivity.this, XmppConnectionService.class));
finish();
});
builder.setPositiveButton(
@ -383,58 +423,89 @@ public abstract class XmppActivity extends ActionBarActivity {
protected void deleteAccount(final Account account, final Runnable postDelete) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
final CheckBox deleteFromServer =
dialogView.findViewById(R.id.delete_from_server);
final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server);
builder.setView(dialogView);
builder.setTitle(R.string.mgmt_account_delete);
builder.setPositiveButton(getString(R.string.delete),null);
builder.setPositiveButton(getString(R.string.delete), null);
builder.setNegativeButton(getString(R.string.cancel), null);
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(dialogInterface->{
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> {
final boolean unregister = deleteFromServer.isChecked();
if (unregister) {
if (account.isOnlineAndConnected()) {
deleteFromServer.setEnabled(false);
button.setText(R.string.please_wait);
button.setEnabled(false);
xmppConnectionService.unregisterAccount(account, result -> {
runOnUiThread(()->{
if (result) {
dialog.dismiss();
if (postDelete != null) {
postDelete.run();
}
if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
final Intent intent = SignupUtils.getSignUpIntent(this);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
dialog.setOnShowListener(
dialogInterface -> {
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(
v -> {
final boolean unregister = deleteFromServer.isChecked();
if (unregister) {
if (account.isOnlineAndConnected()) {
deleteFromServer.setEnabled(false);
button.setText(R.string.please_wait);
button.setEnabled(false);
xmppConnectionService.unregisterAccount(
account,
result -> {
runOnUiThread(
() -> {
if (result) {
dialog.dismiss();
if (postDelete != null) {
postDelete.run();
}
if (xmppConnectionService
.getAccounts()
.size()
== 0
&& Config
.MAGIC_CREATE_DOMAIN
!= null) {
final Intent intent =
SignupUtils
.getSignUpIntent(
this);
intent.setFlags(
Intent
.FLAG_ACTIVITY_NEW_TASK
| Intent
.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
} else {
deleteFromServer.setEnabled(
true);
button.setText(R.string.delete);
button.setEnabled(true);
Toast.makeText(
this,
R.string
.could_not_delete_account_from_server,
Toast
.LENGTH_LONG)
.show();
}
});
});
} else {
Toast.makeText(
this,
R.string.not_connected_try_again,
Toast.LENGTH_LONG)
.show();
}
} else {
deleteFromServer.setEnabled(true);
button.setText(R.string.delete);
button.setEnabled(true);
Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show();
xmppConnectionService.deleteAccount(account);
dialog.dismiss();
if (xmppConnectionService.getAccounts().size() == 0
&& Config.MAGIC_CREATE_DOMAIN != null) {
final Intent intent = SignupUtils.getSignUpIntent(this);
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
} else if (postDelete != null) {
postDelete.run();
}
}
});
});
} else {
Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_LONG).show();
}
} else {
xmppConnectionService.deleteAccount(account);
dialog.dismiss();
if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
final Intent intent = SignupUtils.getSignUpIntent(this);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
} else if (postDelete != null) {
postDelete.run();
}
}
});
});
});
dialog.show();
}
@ -442,61 +513,75 @@ public abstract class XmppActivity extends ActionBarActivity {
protected void registerListeners() {
if (this instanceof XmppConnectionService.OnConversationUpdate) {
this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
this.xmppConnectionService.setOnConversationListChangedListener(
(XmppConnectionService.OnConversationUpdate) this);
}
if (this instanceof XmppConnectionService.OnAccountUpdate) {
this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
this.xmppConnectionService.setOnAccountListChangedListener(
(XmppConnectionService.OnAccountUpdate) this);
}
if (this instanceof XmppConnectionService.OnCaptchaRequested) {
this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
this.xmppConnectionService.setOnCaptchaRequestedListener(
(XmppConnectionService.OnCaptchaRequested) this);
}
if (this instanceof XmppConnectionService.OnRosterUpdate) {
this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
this.xmppConnectionService.setOnRosterUpdateListener(
(XmppConnectionService.OnRosterUpdate) this);
}
if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
this.xmppConnectionService.setOnMucRosterUpdateListener(
(XmppConnectionService.OnMucRosterUpdate) this);
}
if (this instanceof OnUpdateBlocklist) {
this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
}
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
this.xmppConnectionService.setOnShowErrorToastListener(
(XmppConnectionService.OnShowErrorToast) this);
}
if (this instanceof OnKeyStatusUpdated) {
this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
}
if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
this.xmppConnectionService.setOnRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) this);
}
}
protected void unregisterListeners() {
if (this instanceof XmppConnectionService.OnConversationUpdate) {
this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
this.xmppConnectionService.removeOnConversationListChangedListener(
(XmppConnectionService.OnConversationUpdate) this);
}
if (this instanceof XmppConnectionService.OnAccountUpdate) {
this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
this.xmppConnectionService.removeOnAccountListChangedListener(
(XmppConnectionService.OnAccountUpdate) this);
}
if (this instanceof XmppConnectionService.OnCaptchaRequested) {
this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
this.xmppConnectionService.removeOnCaptchaRequestedListener(
(XmppConnectionService.OnCaptchaRequested) this);
}
if (this instanceof XmppConnectionService.OnRosterUpdate) {
this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
this.xmppConnectionService.removeOnRosterUpdateListener(
(XmppConnectionService.OnRosterUpdate) this);
}
if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
this.xmppConnectionService.removeOnMucRosterUpdateListener(
(XmppConnectionService.OnMucRosterUpdate) this);
}
if (this instanceof OnUpdateBlocklist) {
this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this);
}
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
this.xmppConnectionService.removeOnShowErrorToastListener(
(XmppConnectionService.OnShowErrorToast) this);
}
if (this instanceof OnKeyStatusUpdated) {
this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this);
}
if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
this.xmppConnectionService.removeRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) this);
}
}
@ -504,7 +589,9 @@ public abstract class XmppActivity extends ActionBarActivity {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
startActivity(
new Intent(
this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
break;
case R.id.action_privacy_policy:
openPrivacyPolicy();
@ -539,7 +626,8 @@ public abstract class XmppActivity extends ActionBarActivity {
}
}
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
public void selectPresence(
final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
if (conversation.hasValidOtrSession()) {
SessionID id = conversation.getOtrSession().getSessionID();
@ -570,7 +658,8 @@ public abstract class XmppActivity extends ActionBarActivity {
}
} else if (presences.size() == 1) {
final String presence = presences.toResourceArray()[0];
conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence));
conversation.setNextCounterpart(
PresenceSelector.getNextCounterpart(contact, presence));
listener.onPresenceSelected();
} else {
PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
@ -585,7 +674,8 @@ public abstract class XmppActivity extends ActionBarActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
metrics = getResources().getDisplayMetrics();
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
this.isCameraFeatureAvailable =
getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
this.mCustomColors = ThemeHelper.applyCustomColors(this);
}
@ -600,10 +690,12 @@ public abstract class XmppActivity extends ActionBarActivity {
protected boolean isAffectedByDataSaver() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
final ConnectivityManager cm =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
return cm != null
&& cm.isActiveNetworkMetered()
&& Compatibility.getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
&& Compatibility.getRestrictBackgroundStatus(cm)
== ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
} else {
return false;
}
@ -680,9 +772,16 @@ public abstract class XmppActivity extends ActionBarActivity {
switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null);
}
public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit, String thread) {
public void switchToConversation(
Conversation conversation,
String text,
boolean asQuote,
String nick,
boolean pm,
boolean doNotAppend,
String postInit,
String thread) {
if (conversation == null) return;
Intent intent = new Intent(this, ConversationsActivity.class);
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
@ -732,7 +831,10 @@ public abstract class XmppActivity extends ActionBarActivity {
intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
intent.putExtra("init", init);
if (init) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
}
if (fingerprint != null) {
intent.putExtra("fingerprint", fingerprint);
@ -756,85 +858,113 @@ public abstract class XmppActivity extends ActionBarActivity {
}
protected void inviteToConversation(Conversation conversation) {
startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
startActivityForResult(
ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
}
protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) {
protected void announcePgp(
final Account account,
final Conversation conversation,
Intent intent,
final Runnable onSuccess) {
if (account.getPgpId() == 0) {
choosePgpSignId(account);
} else {
final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
xmppConnectionService
.getPgpEngine()
.generateSignature(
intent,
account,
status,
new UiCallback<String>() {
@Override
public void userInputRequired(final PendingIntent pi, final String signature) {
try {
startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0,Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
@Override
public void userInputRequired(
final PendingIntent pi, final String signature) {
try {
startIntentSenderForResult(
pi.getIntentSender(),
REQUEST_ANNOUNCE_PGP,
null,
0,
0,
0,
Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
@Override
public void success(String signature) {
account.setPgpSignature(signature);
xmppConnectionService.databaseBackend.updateAccount(account);
xmppConnectionService.sendPresence(account);
if (conversation != null) {
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
xmppConnectionService.updateConversation(conversation);
refreshUi();
}
if (onSuccess != null) {
runOnUiThread(onSuccess);
}
}
@Override
public void success(String signature) {
account.setPgpSignature(signature);
xmppConnectionService.databaseBackend.updateAccount(account);
xmppConnectionService.sendPresence(account);
if (conversation != null) {
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
xmppConnectionService.updateConversation(conversation);
refreshUi();
}
if (onSuccess != null) {
runOnUiThread(onSuccess);
}
}
@Override
public void error(int error, String signature) {
if (error == 0) {
account.setPgpSignId(0);
account.unsetPgpSignature();
xmppConnectionService.databaseBackend.updateAccount(account);
choosePgpSignId(account);
} else {
displayErrorDialog(error);
}
}
});
@Override
public void error(int error, String signature) {
if (error == 0) {
account.setPgpSignId(0);
account.unsetPgpSignature();
xmppConnectionService.databaseBackend.updateAccount(
account);
choosePgpSignId(account);
} else {
displayErrorDialog(error);
}
}
});
}
}
protected void choosePgpSignId(final Account account) {
xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<>() {
@Override
public void success(final Account a) {
}
xmppConnectionService
.getPgpEngine()
.chooseKey(
account,
new UiCallback<>() {
@Override
public void success(final Account a) {}
@Override
public void error(int errorCode, Account object) {
@Override
public void error(int errorCode, Account object) {}
}
@Override
public void userInputRequired(PendingIntent pi, Account object) {
try {
startIntentSenderForResult(pi.getIntentSender(),
REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
});
@Override
public void userInputRequired(PendingIntent pi, Account object) {
try {
startIntentSenderForResult(
pi.getIntentSender(),
REQUEST_CHOOSE_PGP_ID,
null,
0,
0,
0,
Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
});
}
protected void displayErrorDialog(final int errorCode) {
runOnUiThread(() -> {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(XmppActivity.this);
builder.setTitle(getString(R.string.error));
builder.setMessage(errorCode);
builder.setNeutralButton(R.string.accept, null);
builder.create().show();
});
runOnUiThread(
() -> {
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(XmppActivity.this);
builder.setTitle(getString(R.string.error));
builder.setMessage(errorCode);
builder.setNeutralButton(R.string.accept, null);
builder.create().show();
});
}
protected void showAddToRosterDialog(final Contact contact) {
@ -854,13 +984,15 @@ public abstract class XmppActivity extends ActionBarActivity {
builder.setTitle(contact.getJid().toString());
builder.setMessage(R.string.request_presence_updates);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.request_now,
builder.setPositiveButton(
R.string.request_now,
(dialog, which) -> {
if (xmppConnectionServiceBound) {
xmppConnectionService.sendPresencePacket(contact
.getAccount(), xmppConnectionService
.getPresenceGenerator()
.requestPresenceUpdatesFrom(contact));
xmppConnectionService.sendPresencePacket(
contact.getAccount(),
xmppConnectionService
.getPresenceGenerator()
.requestPresenceUpdatesFrom(contact));
}
});
builder.create().show();
@ -870,7 +1002,11 @@ public abstract class XmppActivity extends ActionBarActivity {
quickEdit(previousValue, callback, hint, false, false);
}
protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) {
protected void quickEdit(
String previousValue,
@StringRes int hint,
OnValueEdited callback,
boolean permitEmpty) {
quickEdit(previousValue, callback, hint, false, permitEmpty);
}
@ -895,9 +1031,12 @@ public abstract class XmppActivity extends ActionBarActivity {
boolean alwaysCallback,
boolean startSelected) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
final DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
final DialogQuickeditBinding binding =
DataBindingUtil.inflate(
getLayoutInflater(), R.layout.dialog_quickedit, null, false);
if (password) {
binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
binding.inputEditText.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
builder.setPositiveButton(R.string.accept, null);
if (hint != 0) {
@ -915,33 +1054,39 @@ public abstract class XmppActivity extends ActionBarActivity {
if (startSelected) {
binding.inputEditText.selectAll();
}
View.OnClickListener clickListener = v -> {
String value = binding.inputEditText.getText().toString();
if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
String error = callback.onValueEdited(value);
if (error != null) {
binding.inputLayout.setError(error);
return;
}
}
SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
dialog.dismiss();
};
View.OnClickListener clickListener =
v -> {
String value = binding.inputEditText.getText().toString();
if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
String error = callback.onValueEdited(value);
if (error != null) {
binding.inputLayout.setError(error);
return;
}
}
SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
dialog.dismiss();
};
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
dialog.dismiss();
}));
dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
.setOnClickListener(
(v -> {
SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
dialog.dismiss();
}));
dialog.setCanceledOnTouchOutside(false);
dialog.setOnDismissListener(dialog1 -> {
SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
});
dialog.setOnDismissListener(
dialog1 -> {
SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
});
}
public boolean hasStoragePermission(int requestCode) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
return false;
} else {
return true;
@ -999,7 +1144,8 @@ public abstract class XmppActivity extends ActionBarActivity {
}
protected boolean manuallyChangePresence() {
return getBooleanPreference(AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
return getBooleanPreference(
AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
}
public boolean unicoloredBG() {
@ -1033,16 +1179,21 @@ public abstract class XmppActivity extends ActionBarActivity {
PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
try {
startIntentSenderForResult(
pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
0, 0, Compatibility.pgpStartIntentSenderOptions());
pgp.getIntentForKey(keyId).getIntentSender(),
0,
null,
0,
0,
0,
Compatibility.pgpStartIntentSenderOptions());
} catch (final Throwable e) {
Log.d(Config.LOGTAG,"could not launch OpenKeyChain", e);
Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onResume(){
protected void onResume() {
super.onResume();
SettingsUtils.applyScreenshotSetting(this);
}
@ -1095,11 +1246,27 @@ public abstract class XmppActivity extends ActionBarActivity {
final int black;
final int white;
if (Activities.isNightMode(this)) {
black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured");
white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured");
black =
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurfaceContainerHighest,
"No surface color configured");
white =
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurfaceInverse,
"No inverse surface color configured");
} else {
black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured");
white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured");
black =
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurfaceInverse,
"No inverse surface color configured");
white =
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurfaceContainerHighest,
"No surface color configured");
}
final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
final ImageView view = new ImageView(this);
@ -1126,7 +1293,10 @@ public abstract class XmppActivity extends ActionBarActivity {
public void loadBitmap(Message message, ImageView imageView) {
Drawable bm;
try {
bm = xmppConnectionService.getFileBackend().getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
bm =
xmppConnectionService
.getFileBackend()
.getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
} catch (IOException e) {
bm = null;
}
@ -1185,7 +1355,8 @@ public abstract class XmppActivity extends ActionBarActivity {
return false;
} else {
jids.add(conversation.getJid().asBareJid());
return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
return service.createAdhocConference(
conversation.getAccount(), null, jids, activity.adhocCallback);
}
}
}

View file

@ -197,9 +197,15 @@ public class ConversationAdapter
if (conversation.getMode() == Conversation.MODE_MULTI) {
viewHolder.binding.senderName.setVisibility(View.VISIBLE);
/*
final String dname = UIHelper.getMessageDisplayName(message);
final String[] words = dname.split("\\s+");
viewHolder.binding.senderName.setText((words.length > 0 ? words[0] : dname) + ':');
final var displayName = UIHelper.getMessageDisplayName(message);
final var displayNameParts = displayName.split("\\s+");
// Skip when nickname only consists of blank chars
if (displayNameParts.length == 0) {
viewHolder.binding.senderName.setText(String.format("%s:", displayName));
} else {
viewHolder.binding.senderName.setText(
String.format("%s:", displayNameParts[0]));
}
*/
viewHolder.binding.senderName.setText(UIHelper.getColoredUsername(activity.xmppConnectionService, message) + ":");
} else {
@ -207,7 +213,8 @@ public class ConversationAdapter
}
} else if (message.getType() != Message.TYPE_STATUS) {
viewHolder.binding.senderName.setVisibility(View.VISIBLE);
viewHolder.binding.senderName.setText(activity.getString(R.string.me) + ':');
viewHolder.binding.senderName.setText(
String.format("%s:", activity.getString(R.string.me)));
} else {
viewHolder.binding.senderName.setVisibility(View.GONE);
}

View file

@ -45,6 +45,8 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
.commit();
return true;
});
up.setVisible(!Strings.isNullOrEmpty(getString(R.string.default_push_server)));
}
@Override

View file

@ -99,6 +99,7 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
if (!sharedPreferences.getBoolean("notifications_from_strangers", true) && sharedPreferences.getString("chat_requests", null) == null) {
chatRequests.setValue("strangers");
}
callIntegration.setVisible(CallIntegration.selfManagedAvailable(requireContext()));
}

View file

@ -53,114 +53,118 @@ import eu.siacs.conversations.xmpp.Jid;
public class ShareUtil {
public static void share(XmppActivity activity, Message message) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
if (message.isGeoUri()) {
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
shareIntent.setType("text/plain");
} else if (!message.isFileOrImage()) {
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
shareIntent.setType("text/plain");
shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, message.getStatus() == Message.STATUS_RECEIVED);
} else {
final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
final var fp = message.getFileParams();
final var name = fp == null ? null : fp.getName();
final var displayName = name == null ? file.getName() : name;
try {
shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file, displayName));
} catch (SecurityException e) {
Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
return;
}
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
String mime = message.getMimeType();
if (mime == null) {
mime = "*/*";
}
shareIntent.setType(mime);
}
try {
activity.startActivity(Intent.createChooser(shareIntent, activity.getText(R.string.share_with)));
} catch (ActivityNotFoundException e) {
//This should happen only on faulty androids because normally chooser is always available
Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
}
}
public static void share(XmppActivity activity, Message message) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
if (message.isGeoUri()) {
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getRawBody());
shareIntent.setType("text/plain");
} else if (!message.isFileOrImage()) {
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getQuoteableBody());
shareIntent.setType("text/plain");
shareIntent.putExtra(
ConversationsActivity.EXTRA_AS_QUOTE,
message.getStatus() == Message.STATUS_RECEIVED);
} else {
final DownloadableFile file =
activity.xmppConnectionService.getFileBackend().getFile(message);
final var fp = message.getFileParams();
final var name = fp == null ? null : fp.getName();
final var displayName = name == null ? file.getName() : name;
try {
shareIntent.putExtra(
Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file, displayName));
} catch (SecurityException e) {
Toast.makeText(
activity,
activity.getString(
R.string.no_permission_to_access_x, file.getAbsolutePath()),
Toast.LENGTH_SHORT)
.show();
return;
}
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
String mime = message.getMimeType();
if (mime == null) {
mime = "*/*";
}
shareIntent.setType(mime);
}
try {
activity.startActivity(
Intent.createChooser(shareIntent, activity.getText(R.string.share_with)));
} catch (ActivityNotFoundException e) {
// This should happen only on faulty androids because normally chooser is always
// available
Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT)
.show();
}
}
public static void copyToClipboard(XmppActivity activity, Message message) {
if (activity.copyTextToClipboard(message.getQuoteableBody(), R.string.message)) {
Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
public static void copyToClipboard(XmppActivity activity, Message message) {
if (activity.copyTextToClipboard(message.getQuoteableBody(), R.string.message)) {
Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT)
.show();
}
}
public static void copyUrlToClipboard(XmppActivity activity, Message message) {
final String url;
final int resId;
if (message.isGeoUri()) {
resId = R.string.location;
url = message.getRawBody();
} else if (message.hasFileOnRemoteHost()) {
resId = R.string.file_url;
url = message.getFileParams().url;
} else {
final Message.FileParams fileParams = message.getFileParams();
url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim();
resId = R.string.file_url;
}
if (activity.copyTextToClipboard(url, resId)) {
Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
public static void copyUrlToClipboard(XmppActivity activity, Message message) {
final String url;
final int resId;
if (message.isGeoUri()) {
resId = R.string.location;
url = message.getRawBody();
} else if (message.hasFileOnRemoteHost()) {
resId = R.string.file_url;
url = message.getFileParams().url;
} else {
final Message.FileParams fileParams = message.getFileParams();
url =
(fileParams != null && fileParams.url != null)
? fileParams.url
: message.getRawBody().trim();
resId = R.string.file_url;
}
if (activity.copyTextToClipboard(url, resId)) {
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())) {
try {
final Jid jid = new XmppUri(uri).getJid();
if (copyTextToClipboard(context, jid.asBareJid().toString(), R.string.account_settings_jabber_id)) {
Toast.makeText(context, R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
} catch (final Exception e) { }
} else {
if (copyTextToClipboard(context, url, R.string.web_address)) {
Toast.makeText(context, 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())) {
try {
final Jid jid = new XmppUri(uri).getJid();
if (copyTextToClipboard(context, jid.asBareJid().toString(), R.string.account_settings_jabber_id)) {
Toast.makeText(context, R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
} catch (final Exception e) { }
} else {
if (copyTextToClipboard(context, url, R.string.web_address)) {
Toast.makeText(context, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
}
public static void copyLinkToClipboard(final XmppActivity activity, final Message message) {
final SpannableStringBuilder body = message.getMergedBody();
MyLinkify.addLinks(body, true);
for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) {
copyLinkToClipboard(activity, urlspan.getURL());
return;
}
}
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());
return;
}
}
public static boolean containsXmppUri(String body) {
Matcher xmppPatternMatcher = Patterns.XMPP_PATTERN.matcher(body);
if (xmppPatternMatcher.find()) {
try {
return new XmppUri(body.substring(xmppPatternMatcher.start(), xmppPatternMatcher.end())).isValidJid();
} catch (Exception e) {
return false;
}
}
return false;
}
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 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 String getLinkScheme(final SpannableStringBuilder body) {
MyLinkify.addLinks(body, false);

View file

@ -1,8 +1,70 @@
package eu.siacs.conversations.utils;
import net.fellbaum.jemoji.EmojiManager;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
public class Emoticons {
private static final int VARIATION_16 = 0xFE0F;
private static final int VARIATION_15 = 0xFE0E;
private static final String VARIATION_16_STRING = new String(new char[] {VARIATION_16});
private static final String VARIATION_15_STRING = new String(new char[] {VARIATION_15});
private static final Set<String> TEXT_DEFAULT_TO_VS16 =
ImmutableSet.of(
"",
"",
"",
"",
"",
"",
"",
"",
"\uD83C\uDF96",
"\uD83C\uDFC6",
"\uD83E\uDD47",
"\uD83E\uDD48",
"\uD83E\uDD49",
"\uD83D\uDC51",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"");
public static String normalizeToVS16(final String input) {
return TEXT_DEFAULT_TO_VS16.contains(input) && !input.endsWith(VARIATION_15_STRING)
? input + VARIATION_16_STRING
: input;
}
public static String existingVariant(final String original, final Set<String> existing) {
if (existing.contains(original) || original.endsWith(VARIATION_15_STRING)) {
return original;
}
final var variant =
original.endsWith(VARIATION_16_STRING)
? original.substring(0, original.length() - 1)
: original + VARIATION_16_STRING;
return existing.contains(variant) ? variant : original;
}
public static boolean isEmoji(String input) {
return EmojiManager.isEmoji(input);
}

View file

@ -30,16 +30,14 @@
package eu.siacs.conversations.utils;
import com.google.common.base.Strings;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Pattern;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.AesGcmURL;
import eu.siacs.conversations.http.URL;
import eu.siacs.conversations.ui.util.QuoteHelper;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Pattern;
public class MessageUtils {
@ -47,7 +45,7 @@ public class MessageUtils {
public static final String EMPTY_STRING = "";
public static String prepareQuote(Message message) {
public static String prepareQuote(final Message message) {
final StringBuilder builder = new StringBuilder();
final String body;
if (message.hasMeCommand()) {
@ -102,8 +100,12 @@ public class MessageUtils {
final String protocol = uri.getScheme();
final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches();
final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol);
final boolean validAesGcm =
AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol)
&& encrypted
&& (lines.length == 1 || followedByDataUri);
final boolean validProtocol =
"http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol);
final boolean validOob = validProtocol && (oob || encrypted || (legacyEncryption && uri.getPath() != null && (uri.getPath().endsWith(".xdc") || uri.getPath().endsWith(".webp") || uri.getPath().endsWith(".gif") || uri.getPath().endsWith(".png")))) && lines.length == 1;
return validAesGcm || validOob;
}
@ -140,7 +142,10 @@ public class MessageUtils {
}
public static boolean unInitiatedButKnownSize(Message message) {
return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().url != null &&
(message.getFileParams().size != null || (message.getOob() != null && message.getOob().getScheme() != null && message.getOob().getScheme().equalsIgnoreCase("cid")));
return message.getType() == Message.TYPE_TEXT
&& message.getTransferable() == null
&& message.isOOb()
&& (message.getFileParams().size != null || (message.getOob() != null && message.getOob().getScheme() != null && message.getOob().getScheme().equalsIgnoreCase("cid")))
&& message.getFileParams().url != null;
}
}

View file

@ -3,13 +3,12 @@ package eu.siacs.conversations.utils;
import android.content.ContentValues;
import android.database.Cursor;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
@ -54,6 +53,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.Conversations;
import eu.siacs.conversations.R;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
@ -64,6 +64,7 @@ import org.minidns.DnsClient;
import org.minidns.cache.LruCache;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsname.InvalidDnsNameException;
import org.minidns.dnssec.DnssecResultNotAuthenticException;
import org.minidns.dnssec.DnssecValidationFailedException;
import org.minidns.dnsserverlookup.AndroidUsingExec;
@ -108,7 +109,7 @@ public class Resolver {
return left.ip != null ? -1 : 1;
}
} else {
return left.directTls ? -1 : 1;
return left.directTls ? 1 : -1;
}
} else {
return left.priority - right.priority;
@ -117,7 +118,8 @@ public class Resolver {
private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12);
public static final int DEFAULT_PORT_XMPP = 5222;
public static final int XMPP_PORT_STARTTLS = 5222;
private static final int XMPP_PORT_DIRECT_TLS = 5223;
private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
private static final String STARTTLS_SERVICE = "_xmpp-client";
@ -295,7 +297,7 @@ public class Resolver {
}
public static boolean useDirectTls(final int port) {
return port == 443 || port == 5223;
return port == 443 || port == XMPP_PORT_DIRECT_TLS;
}
public static List<Result> resolve(final String domain) {
@ -342,14 +344,11 @@ public class Resolver {
if (IP.matches(domain)) {
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(domain);
} catch (final UnknownHostException e) {
inetAddress = InetAddresses.forString(domain);
} catch (final IllegalArgumentException e) {
return Collections.emptyList();
}
final Result result = new Result();
result.ip = inetAddress;
result.port = DEFAULT_PORT_XMPP;
return Collections.singletonList(result);
return Result.createWithDefaultPorts(null, inetAddress);
} else {
return Collections.emptyList();
}
@ -488,7 +487,7 @@ public class Resolver {
noSrvFallbacks,
results -> {
if (results.isEmpty()) {
return Collections.singletonList(Result.createDefault(dnsName));
return Result.createDefaults(dnsName);
} else {
return results;
}
@ -535,7 +534,7 @@ public class Resolver {
public static final String AUTHENTICATED = "authenticated";
private InetAddress ip;
private DnsName hostname;
private int port = DEFAULT_PORT_XMPP;
private int port = XMPP_PORT_STARTTLS;
private boolean directTls = false;
private boolean authenticated = false;
private int priority;
@ -549,17 +548,40 @@ public class Resolver {
return result;
}
static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
static List<Result> createWithDefaultPorts(final DnsName hostname, final InetAddress ip) {
return Lists.transform(
Arrays.asList(XMPP_PORT_STARTTLS),
p -> createDefault(hostname, ip, p, false));
}
static Result createDefault(final DnsName hostname, final InetAddress ip, final int port, final boolean authenticated) {
Result result = new Result();
result.port = DEFAULT_PORT_XMPP;
result.port = port;
result.hostname = hostname;
result.ip = ip;
result.authenticated = authenticated;
return result;
}
static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
return createDefault(hostname, ip, XMPP_PORT_STARTTLS, authenticated);
}
static Result createDefault(final DnsName hostname) {
return createDefault(hostname, null, false);
return createDefault(hostname, null, XMPP_PORT_STARTTLS, false);
}
static List<Result> createDefaults(
final DnsName hostname, final Collection<InetAddress> inetAddresses) {
final ImmutableList.Builder<Result> builder = new ImmutableList.Builder<>();
for (final InetAddress inetAddress : inetAddresses) {
builder.addAll(createWithDefaultPorts(hostname, inetAddress));
}
return builder.build();
}
static List<Result> createDefaults(final DnsName hostname) {
return createWithDefaultPorts(hostname, null);
}
public static Result fromCursor(final Cursor cursor) {
@ -630,6 +652,10 @@ public class Resolver {
.toString();
}
public String asDestination() {
return ip != null ? InetAddresses.toAddrString(ip) : hostname.toString();
}
public ContentValues toContentValues() {
final ContentValues contentValues = new ContentValues();
contentValues.put(IP, ip == null ? null : ip.getAddress());

View file

@ -29,7 +29,6 @@
package eu.siacs.conversations.utils;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.preference.PreferenceManager;
@ -47,12 +46,9 @@ import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;
import com.google.android.material.color.MaterialColors;
import eu.siacs.conversations.ui.text.QuoteSpan;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -64,150 +60,162 @@ import eu.siacs.conversations.ui.text.QuoteSpan;
public class StylingHelper {
public static final int XHTML_IGNORE = 1;
public static final int XHTML_REMOVE = 2;
public static final int XHTML_EMPHASIS = 3;
public static final int XHTML_CODE = 4;
public static final int NOLINKIFY = 0xf0;
public static final int XHTML_IGNORE = 1;
public static final int XHTML_REMOVE = 2;
public static final int XHTML_EMPHASIS = 3;
public static final int XHTML_CODE = 4;
public static final int NOLINKIFY = 0xf0;
private static final List<? extends Class<? extends ParcelableSpan>> SPAN_CLASSES = Arrays.asList(
StyleSpan.class,
StrikethroughSpan.class,
TypefaceSpan.class,
ForegroundColorSpan.class,
RelativeSizeSpan.class
);
private static final List<? extends Class<? extends ParcelableSpan>> SPAN_CLASSES =
Arrays.asList(
StyleSpan.class,
StrikethroughSpan.class,
TypefaceSpan.class,
RelativeSizeSpan.class,
ForegroundColorSpan.class);
public static void clear(final Editable editable) {
final int end = editable.length() - 1;
for (Class<? extends ParcelableSpan> clazz : SPAN_CLASSES) {
for (ParcelableSpan span : editable.getSpans(0, end, clazz)) {
editable.removeSpan(span);
}
}
}
public static void clear(final Editable editable) {
final int end = editable.length() - 1;
for (Class<? extends ParcelableSpan> clazz : SPAN_CLASSES) {
for (ParcelableSpan span : editable.getSpans(0, end, clazz)) {
editable.removeSpan(span);
}
}
}
public static void format(final Editable editable, int start, int end, @ColorInt int textColor, final boolean composing) {
for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) {
final int keywordLength = style.getKeyword().length();
int keywordLengthStart = keywordLength;
if ("```".equals(style.getKeyword())) {
int i;
for (i = style.getStart(); i < editable.length(); i++) {
if (editable.charAt(i) == '\n') break;
}
keywordLengthStart = i - style.getStart() + 1;
}
public static void format(final Editable editable, int start, int end, @ColorInt int textColor, final boolean composing) {
for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) {
final int keywordLength = style.getKeyword().length();
int keywordLengthStart = keywordLength;
if ("```".equals(style.getKeyword())) {
int i;
for (i = style.getStart(); i < editable.length(); i++) {
if (editable.charAt(i) == '\n') break;
}
keywordLengthStart = i - style.getStart() + 1;
}
editable.setSpan(
createSpanForStyle(style),
style.getStart() + keywordLengthStart,
style.getEnd() - keywordLength + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE |
("*".equals(style.getKeyword()) || "_".equals(style.getKeyword()) ? XHTML_EMPHASIS << Spanned.SPAN_USER_SHIFT : 0) |
("```".equals(style.getKeyword()) && keywordLengthStart > 4 ? XHTML_CODE << Spanned.SPAN_USER_SHIFT : 0) |
("`".equals(style.getKeyword()) || "```".equals(style.getKeyword()) ? NOLINKIFY << Spanned.SPAN_USER_SHIFT : 0)
);
makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLengthStart, textColor, composing);
makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor, composing);
}
}
editable.setSpan(
createSpanForStyle(style),
style.getStart() + keywordLengthStart,
style.getEnd() - keywordLength + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE |
("*".equals(style.getKeyword()) || "_".equals(style.getKeyword()) ? XHTML_EMPHASIS << Spanned.SPAN_USER_SHIFT : 0) |
("```".equals(style.getKeyword()) && keywordLengthStart > 4 ? XHTML_CODE << Spanned.SPAN_USER_SHIFT : 0) |
("`".equals(style.getKeyword()) || "```".equals(style.getKeyword()) ? NOLINKIFY << Spanned.SPAN_USER_SHIFT : 0)
);
makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLengthStart, textColor, composing);
makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor, composing);
}
}
public static void format(final Editable editable, @ColorInt int textColor) {
format(editable, textColor, false);
}
public static void format(final Editable editable, @ColorInt int textColor) {
format(editable, textColor, false);
}
public static void format(final Editable editable, @ColorInt int textColor, final boolean composing) {
int end = 0;
Message.MergeSeparator[] spans = editable.getSpans(0, editable.length() - 1, Message.MergeSeparator.class);
for (Message.MergeSeparator span : spans) {
format(editable, end, editable.getSpanStart(span), textColor, composing);
end = editable.getSpanEnd(span);
}
format(editable, end, editable.length() - 1, textColor, composing);
}
public static void format(final Editable editable, @ColorInt int textColor, final boolean composing) {
int end = 0;
format(editable, end, editable.length() - 1, textColor, composing);
}
public static void highlight(final TextView view, final Editable editable, final List<String> needles) {
for (final String needle : needles) {
if (!FtsUtils.isKeyword(needle)) {
highlight(view, editable, needle);
}
}
}
public static void highlight(
final TextView view, final Editable editable, final List<String> needles) {
for (final String needle : needles) {
if (!FtsUtils.isKeyword(needle)) {
highlight(view, editable, needle);
}
}
}
public static List<String> filterHighlightedWords(List<String> terms) {
List<String> words = new ArrayList<>();
for (String term : terms) {
if (!FtsUtils.isKeyword(term)) {
StringBuilder builder = new StringBuilder();
for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) {
codepoint = term.codePointAt(i);
if (Character.isLetterOrDigit(codepoint)) {
builder.append(Character.toChars(codepoint));
} else if (builder.length() > 0) {
words.add(builder.toString());
builder.delete(0, builder.length());
}
}
if (builder.length() > 0) {
words.add(builder.toString());
}
}
}
return words;
}
public static List<String> filterHighlightedWords(List<String> terms) {
List<String> words = new ArrayList<>();
for (String term : terms) {
if (!FtsUtils.isKeyword(term)) {
StringBuilder builder = new StringBuilder();
for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) {
codepoint = term.codePointAt(i);
if (Character.isLetterOrDigit(codepoint)) {
builder.append(Character.toChars(codepoint));
} else if (builder.length() > 0) {
words.add(builder.toString());
builder.delete(0, builder.length());
}
}
if (builder.length() > 0) {
words.add(builder.toString());
}
}
}
return words;
}
private static void highlight(final TextView view, final Editable editable, final String needle) {
final int length = needle.length();
String string = editable.toString();
int start = indexOfIgnoreCase(string, needle, 0);
while (start != -1) {
int end = start + length;
editable.setSpan(new BackgroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimaryFixedDim)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
editable.setSpan(new ForegroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryFixed)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
start = indexOfIgnoreCase(string, needle, start + length);
}
private static void highlight(
final TextView view, final Editable editable, final String needle) {
final int length = needle.length();
String string = editable.toString();
int start = indexOfIgnoreCase(string, needle, 0);
while (start != -1) {
int end = start + length;
editable.setSpan(
new BackgroundColorSpan(
MaterialColors.getColor(
view, com.google.android.material.R.attr.colorPrimaryFixedDim)),
start,
end,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
editable.setSpan(
new ForegroundColorSpan(
MaterialColors.getColor(
view, com.google.android.material.R.attr.colorOnPrimaryFixed)),
start,
end,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
start = indexOfIgnoreCase(string, needle, start + length);
}
}
}
static CharSequence subSequence(CharSequence charSequence, int start, int end) {
if (start == 0 && charSequence.length() + 1 == end) {
return charSequence;
}
if (charSequence instanceof Spannable spannable) {
Spannable sub = (Spannable) spannable.subSequence(start, end);
for (Class<? extends ParcelableSpan> clazz : SPAN_CLASSES) {
ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz);
for (ParcelableSpan parcelableSpan : spannables) {
int beginSpan = spannable.getSpanStart(parcelableSpan);
int endSpan = spannable.getSpanEnd(parcelableSpan);
if (beginSpan >= start && endSpan <= end) {
continue;
}
sub.setSpan(
clone(parcelableSpan),
Math.max(beginSpan - start, 0),
Math.min(sub.length() - 1, endSpan),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return sub;
} else {
return charSequence.subSequence(start, end);
}
}
static CharSequence subSequence(CharSequence charSequence, int start, int end) {
if (start == 0 && charSequence.length() + 1 == end) {
return charSequence;
}
if (charSequence instanceof Spannable spannable) {
Spannable sub = (Spannable) spannable.subSequence(start, end);
for (Class<? extends ParcelableSpan> clazz : SPAN_CLASSES) {
ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz);
for (ParcelableSpan parcelableSpan : spannables) {
int beginSpan = spannable.getSpanStart(parcelableSpan);
int endSpan = spannable.getSpanEnd(parcelableSpan);
if (beginSpan >= start && endSpan <= end) {
continue;
}
sub.setSpan(clone(parcelableSpan), Math.max(beginSpan - start, 0), Math.min(Math.max(sub.length() - 1, 0), endSpan), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return sub;
} else {
return charSequence.subSequence(start, end);
}
}
private static ParcelableSpan clone(ParcelableSpan span) {
if (span instanceof ForegroundColorSpan) {
return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor());
} else if (span instanceof TypefaceSpan) {
return new TypefaceSpan(((TypefaceSpan) span).getFamily());
} else if (span instanceof StyleSpan) {
return new StyleSpan(((StyleSpan) span).getStyle());
} else if (span instanceof StrikethroughSpan) {
return new StrikethroughSpan();
} else {
throw new AssertionError("Unknown Span");
}
}
private static ParcelableSpan clone(ParcelableSpan span) {
if (span instanceof ForegroundColorSpan) {
return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor());
} else if (span instanceof TypefaceSpan) {
return new TypefaceSpan(((TypefaceSpan) span).getFamily());
} else if (span instanceof StyleSpan) {
return new StyleSpan(((StyleSpan) span).getStyle());
} else if (span instanceof StrikethroughSpan) {
return new StrikethroughSpan();
} else {
throw new AssertionError("Unknown Span");
}
}
private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) {
private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) {
return switch (style.getKeyword()) {
case "*" -> new StyleSpan(Typeface.BOLD);
case "_" -> new StyleSpan(Typeface.ITALIC);
@ -215,78 +223,74 @@ public class StylingHelper {
case "`", "```" -> new TypefaceSpan("monospace");
default -> throw new AssertionError("Unknown Style");
};
}
}
private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor, final boolean composing) {
QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class);
@ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor;
@ColorInt int keywordColor = transformColor(textColor);
if (composing) {
if (end-start > 1) {
editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
} else {
editable.setSpan(new RelativeSizeSpan(0), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
}
} else {
editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor, final boolean composing) {
QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class);
@ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor;
@ColorInt int keywordColor = transformColor(textColor);
if (composing) {
if (end-start > 1) {
editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
} else {
editable.setSpan(new RelativeSizeSpan(0), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
}
} else {
editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private static
@ColorInt
int transformColor(@ColorInt int c) {
return Color.argb(Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c));
}
private static @ColorInt int transformColor(@ColorInt int c) {
return Color.argb(
Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c));
}
private static int indexOfIgnoreCase(final String haystack, final String needle, final int start) {
if (haystack == null || needle == null) {
return -1;
}
final int endLimit = haystack.length() - needle.length() + 1;
if (start > endLimit) {
return -1;
}
if (needle.length() == 0) {
return start;
}
for (int i = start; i < endLimit; i++) {
if (haystack.regionMatches(true, i, needle, 0, needle.length())) {
return i;
}
}
return -1;
}
private static int indexOfIgnoreCase(
final String haystack, final String needle, final int start) {
if (haystack == null || needle == null) {
return -1;
}
final int endLimit = haystack.length() - needle.length() + 1;
if (start > endLimit) {
return -1;
}
if (needle.length() == 0) {
return start;
}
for (int i = start; i < endLimit; i++) {
if (haystack.regionMatches(true, i, needle, 0, needle.length())) {
return i;
}
}
return -1;
}
public static class MessageEditorStyler implements TextWatcher {
public static class MessageEditorStyler implements TextWatcher {
private final EditText mEditText;
private final MessageAdapter mAdapter;
private final EditText mEditText;
private final MessageAdapter mAdapter;
public MessageEditorStyler(EditText editText, MessageAdapter adapter) {
this.mEditText = editText;
this.mAdapter = adapter;
}
public MessageEditorStyler(EditText editText, MessageAdapter adapter) {
this.mEditText = editText;
this.mAdapter = adapter;
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
clear(editable);
final var p = PreferenceManager.getDefaultSharedPreferences(mEditText.getContext());
if (!p.getBoolean("compose_rich_text", mEditText.getContext().getResources().getBoolean(R.bool.compose_rich_text))) return;
for (final var span : editable.getSpans(0, editable.length() - 1, QuoteSpan.class)) {
editable.removeSpan(span);
}
format(editable, mEditText.getCurrentTextColor(), true);
mAdapter.handleTextQuotes(mEditText, editable, false);
}
}
@Override
public void afterTextChanged(Editable editable) {
clear(editable);
final var p = PreferenceManager.getDefaultSharedPreferences(mEditText.getContext());
if (!p.getBoolean("compose_rich_text", mEditText.getContext().getResources().getBoolean(R.bool.compose_rich_text))) return;
for (final var span : editable.getSpans(0, editable.length() - 1, QuoteSpan.class)) {
editable.removeSpan(span);
}
format(editable, mEditText.getCurrentTextColor(), true);
mAdapter.handleTextQuotes(mEditText, editable, false);
}
}
}

View file

@ -12,7 +12,6 @@ import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -21,6 +20,8 @@ import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import org.xmlpull.v1.XmlPullParserException;
@ -57,6 +58,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import javax.net.ssl.KeyManager;
@ -76,8 +78,10 @@ import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.sasl.ChannelBinding;
import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
import eu.siacs.conversations.crypto.sasl.DowngradeProtection;
import eu.siacs.conversations.crypto.sasl.HashedToken;
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramMechanism;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
@ -109,13 +113,13 @@ import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.bind.Bind2;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import im.conversations.android.xmpp.model.AuthenticationFailure;
import im.conversations.android.xmpp.model.AuthenticationRequest;
import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
import im.conversations.android.xmpp.model.StreamElement;
import im.conversations.android.xmpp.model.bind2.Bind;
import im.conversations.android.xmpp.model.bind2.Bound;
import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
import im.conversations.android.xmpp.model.csi.Active;
import im.conversations.android.xmpp.model.csi.Inactive;
import im.conversations.android.xmpp.model.error.Condition;
@ -142,54 +146,12 @@ 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.StreamError;
import im.conversations.android.xmpp.model.tls.Proceed;
import im.conversations.android.xmpp.model.tls.StartTls;
import im.conversations.android.xmpp.processor.BindProcessor;
import okhttp3.HttpUrl;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
public class XmppConnection implements Runnable {
protected final Account account;
@ -218,7 +180,7 @@ public class XmppConnection implements Runnable {
private int stanzasSentBeforeAuthentication;
private long lastPacketReceived = 0;
private long lastPingSent = 0;
private long lastConnect = 0;
private long lastConnectionStarted = 0;
private long lastSessionStarted = 0;
private long lastDiscoStarted = 0;
private boolean isMamPreferenceAlways = false;
@ -271,10 +233,10 @@ public class XmppConnection implements Runnable {
}
}
private static boolean validBase64(String input) {
private static boolean validBase64(final String input) {
try {
return Base64.decode(input, Base64.URL_SAFE).length == 3;
} catch (Throwable throwable) {
} catch (final Throwable throwable) {
return false;
}
}
@ -327,7 +289,7 @@ public class XmppConnection implements Runnable {
}
public void prepareNewConnection() {
this.lastConnect = SystemClock.elapsedRealtime();
this.lastConnectionStarted = SystemClock.elapsedRealtime();
this.lastPingSent = SystemClock.elapsedRealtime();
this.lastDiscoStarted = Long.MAX_VALUE;
this.mWaitingForSmCatchup.set(false);
@ -354,43 +316,53 @@ public class XmppConnection implements Runnable {
this.quickStartInProgress = false;
this.isBound = false;
this.attempt++;
this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified
this.dane = false;
// with dnssec
this.currentResolverResult = null;
// will be set if user entered hostname is being used or hostname was verified with dnssec
this.verifiedHostname = null;
try {
Socket localSocket;
shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
this.changeStatus(Account.State.CONNECTING);
final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
// TODO collapse Tor usage into normal connection code path
if (useTor) {
String destination;
if (account.getHostname().isEmpty() || account.isOnion()) {
destination = account.getServer();
final var seeOtherHost = this.seeOtherHostResolverResult;
final var hostname = account.getHostname().trim();
final var port = account.getPort();
final Resolver.Result resume = streamId == null ? null : streamId.location;
final Resolver.Result viaTor;
if (resume != null) {
viaTor = resume;
} else if (seeOtherHost != null) {
viaTor = seeOtherHost;
} else if (hostname.isEmpty() || port < 0) {
viaTor =
Iterables.getOnlyElement(
Resolver.fromHardCoded(
account.getServer(), Resolver.XMPP_PORT_STARTTLS));
} else {
destination = account.getHostname();
this.verifiedHostname = destination;
viaTor = Iterables.getOnlyElement(Resolver.fromHardCoded(hostname, port));
this.verifiedHostname = hostname;
}
final int port = account.getPort();
final boolean directTls = Resolver.useDirectTls(port);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " via Tor: " + viaTor);
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": connect to "
+ destination
+ " via Tor. directTls="
+ directTls);
localSocket = SocksSocketFactory.createSocketOverTor(destination, port);
localSocket =
SocksSocketFactory.createSocketOverTor(
viaTor.asDestination(), viaTor.getPort());
if (directTls) {
if (viaTor.isDirectTls()) {
localSocket = upgradeSocketToTls(localSocket);
features.encryptionEnabled = true;
}
try {
startXmpp(localSocket);
if (startXmpp(localSocket)) {
this.currentResolverResult = viaTor;
this.seeOtherHostResolverResult = null;
}
} catch (final InterruptedException e) {
Log.d(
Config.LOGTAG,
@ -401,12 +373,12 @@ public class XmppConnection implements Runnable {
throw new IOException("Could not start stream", e);
}
} else {
final var hostname = account.getHostname().trim();
final String domain = account.getServer();
final List<Resolver.Result> results = new ArrayList<>();
final boolean hardcoded = extended && !account.getHostname().isEmpty();
final boolean hardcoded = extended && !hostname.isEmpty();
if (hardcoded) {
results.addAll(
Resolver.fromHardCoded(account.getHostname(), account.getPort()));
results.addAll(Resolver.fromHardCoded(hostname, account.getPort()));
} else {
results.addAll(Resolver.resolve(domain));
}
@ -499,16 +471,13 @@ public class XmppConnection implements Runnable {
localSocket = new Socket();
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
if (features.encryptionEnabled) {
localSocket = upgradeSocketToTls(localSocket);
}
localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
if (startXmpp(localSocket)) {
localSocket.setSoTimeout(
0); // reset to 0; once the connection is established we dont
// want this
// reset to 0; once the connection is established we don't want this
localSocket.setSoTimeout(0);
if (!hardcoded && !result.equals(storedBackupResult)) {
mXmppConnectionService.databaseBackend.saveResolverResult(
domain, result);
@ -581,15 +550,17 @@ public class XmppConnection implements Runnable {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
// this means we have at least found a socket to connect to. give the connection another 90s
this.lastConnectionStarted = SystemClock.elapsedRealtime();
this.socket = socket;
tagReader = new XmlReader();
this.tagReader = new XmlReader();
if (tagWriter != null) {
tagWriter.forceClose();
}
tagWriter = new TagWriter();
tagWriter.setOutputStream(socket.getOutputStream());
tagReader.setInputStream(socket.getInputStream());
tagWriter.beginDocument();
this.tagWriter = new TagWriter();
this.tagWriter.setOutputStream(socket.getOutputStream());
this.tagReader.setInputStream(socket.getInputStream());
this.tagWriter.beginDocument();
final boolean quickStart;
if (socket instanceof SSLSocket sslSocket) {
SSLSockets.log(account, sslSocket);
@ -661,24 +632,28 @@ public class XmppConnection implements Runnable {
this.mStreamCountDownLatch = streamCountDownLatch;
Tag nextTag = tagReader.readTag();
while (nextTag != null && !nextTag.isEnd("stream")) {
if (nextTag.isStart("error")) {
processStreamError(nextTag);
if (nextTag.isStart("error", Namespace.STREAMS)) {
processStreamError(tagReader.readElement(nextTag, StreamError.class));
} else if (nextTag.isStart("features", Namespace.STREAMS)) {
processStreamFeatures(nextTag);
} else if (nextTag.isStart("proceed", Namespace.TLS)) {
switchOverToTls(nextTag);
} else if (nextTag.isStart("failure", Namespace.TLS)) {
throw new StateChangingException(Account.State.TLS_ERROR);
} else if (!isSecure()) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
} else if (account.isOptionSet(Account.OPTION_REGISTER)
&& nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
processIq(nextTag);
} else if (!isSecure() || this.loginInfo == null) {
} else if (this.loginInfo == null) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
} else if (nextTag.isStart("success")) {
final Element success = tagReader.readElement(nextTag);
if (processSuccess(success)) {
break;
}
} else if (nextTag.isStart("success", Namespace.SASL)) {
processSuccess(tagReader.readElement(nextTag, Success.class));
break;
} else if (nextTag.isStart("success", Namespace.SASL_2)) {
processSuccess(
tagReader.readElement(
nextTag, im.conversations.android.xmpp.model.sasl2.Success.class));
} else if (nextTag.isStart("failure", Namespace.SASL)) {
final var failure = tagReader.readElement(nextTag, Failure.class);
processFailure(failure);
@ -818,7 +793,7 @@ public class XmppConnection implements Runnable {
tagWriter.writeElement(response);
}
private boolean processSuccess(final Element element)
private void processSuccess(final StreamElement element)
throws IOException, XmlPullParserException {
final LoginInfo currentLoginInfo = this.loginInfo;
final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
@ -967,12 +942,9 @@ public class XmppConnection implements Runnable {
final Tag tag = tagReader.readTag();
if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
processStream();
return true;
} else {
throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
}
} else {
return false;
}
}
@ -1080,7 +1052,8 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": fast authentication failed. falling back to regular authentication");
+ ": fast authentication failed. falling back to regular"
+ " authentication");
authenticate();
} else {
throw new StateChangingException(Account.State.UNAUTHORIZED);
@ -1184,7 +1157,15 @@ public class XmppConnection implements Runnable {
}
sendPacket(packet);
}
changeStatusToOnline();
if (mWaitForDisco.get()) {
this.lastDiscoStarted = SystemClock.elapsedRealtime();
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": awaiting disco results after resume");
changeStatus(Account.State.CONNECTING);
} else {
changeStatusToOnline();
}
}
private void changeStatusToOnline() {
@ -1623,13 +1604,22 @@ public class XmppConnection implements Runnable {
authElement = this.streamFeatures.getExtension(Authentication.class);
}
final Collection<String> mechanisms = authElement.getMechanismNames();
final Element cbElement =
this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
final var cbExtension = this.streamFeatures.getExtension(SaslChannelBinding.class);
final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbExtension);
final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
final SaslMechanism saslMechanism =
factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
this.validate(saslMechanism, mechanisms);
final DowngradeProtection downgradeProtection;
if (cbExtension != null) {
downgradeProtection =
new DowngradeProtection(mechanisms, cbExtension.getChannelBindingTypes());
} else {
downgradeProtection = new DowngradeProtection(mechanisms);
}
if (saslMechanism instanceof ScramMechanism scramMechanism) {
scramMechanism.setDowngradeProtection(downgradeProtection);
}
final boolean quickStartAvailable;
final String firstMessage =
saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
@ -1654,6 +1644,11 @@ public class XmppConnection implements Runnable {
hashTokenRequest =
HashedToken.Mechanism.best(
inline.getFastMechanisms(), SSLSockets.version(this.socket));
// TODO warn or fail early if channel binding priority isnt high enough compared to
// login mechanism
// ChannelBinding.priority(hashTokenRequest.channelBinding)
// <
// ChannelBindingMechanism.getPriority(saslMechanism)
} else {
hashTokenRequest = null;
}
@ -1669,7 +1664,8 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": interrupted while waiting for DB restore during SASL2 bind");
+ ": interrupted while waiting for DB restore during SASL2"
+ " bind");
return;
}
}
@ -2188,9 +2184,9 @@ public class XmppConnection implements Runnable {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
mPendingServiceDiscoveries.set(0);
mWaitForDisco.set(waitForDisco);
lastDiscoStarted = SystemClock.elapsedRealtime();
this.lastDiscoStarted = SystemClock.elapsedRealtime();
mXmppConnectionService.scheduleWakeUpCall(
Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode());
final Element caps = streamFeatures.findChild("c");
final String hash = caps == null ? null : caps.getAttribute("hash");
final String ver = caps == null ? null : caps.getAttribute("ver");
@ -2422,14 +2418,11 @@ public class XmppConnection implements Runnable {
});
}
private void processStreamError(final Tag currentTag) throws IOException {
final Element streamError = tagReader.readElement(currentTag);
if (streamError == null) {
return;
}
if (streamError.hasChild("conflict")) {
final var loginInfo = this.loginInfo;
if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
private void processStreamError(final StreamError streamError) throws IOException {
final var loginInfo = this.loginInfo;
final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo);
if (isSecureLoggedIn && streamError.hasChild("conflict")) {
if (loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
this.appSettings.resetInstallationId();
}
account.setResource(createNewResource());
@ -2443,10 +2436,12 @@ public class XmppConnection implements Runnable {
} else if (streamError.hasChild("host-unknown")) {
throw new StateChangingException(Account.State.HOST_UNKNOWN);
} else if (streamError.hasChild("policy-violation")) {
this.lastConnect = SystemClock.elapsedRealtime();
this.lastConnectionStarted = SystemClock.elapsedRealtime();
final String text = streamError.findChildContent("text");
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
failPendingMessages(text);
if (isSecureLoggedIn) {
failPendingMessages(text);
}
throw new StateChangingException(Account.State.POLICY_VIOLATION);
} else if (streamError.hasChild("see-other-host")) {
final String seeOtherHost = streamError.findChildContent("see-other-host");
@ -2760,11 +2755,11 @@ public class XmppConnection implements Runnable {
}
public Jid findDiscoItemByFeature(final String feature) {
final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
if (items.size() >= 1) {
return items.get(0).getKey();
final var items = findDiscoItemsByFeature(feature);
if (items.isEmpty()) {
return null;
}
return null;
return Iterables.getFirst(items, null).getKey();
}
public boolean r() {
@ -2799,8 +2794,7 @@ public class XmppConnection implements Runnable {
}
public String getMucServer() {
List<String> servers = getMucServers();
return servers.size() > 0 ? servers.get(0) : null;
return Iterables.getFirst(getMucServers(), null);
}
public int getTimeToNextAttempt(final boolean aggressive) {
@ -2812,9 +2806,8 @@ public class XmppConnection implements Runnable {
account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
}
final int secondsSinceLast =
(int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
return interval - secondsSinceLast;
final var connectionDuration = Ints.saturatedCast(getConnectionDuration() / 1000);
return interval - connectionDuration;
}
public int getAttempt() {
@ -2830,18 +2823,18 @@ public class XmppConnection implements Runnable {
return System.currentTimeMillis() - diff;
}
public long getLastConnect() {
return this.lastConnect;
public long getConnectionDuration() {
return SystemClock.elapsedRealtime() - this.lastConnectionStarted;
}
public long getDiscoDuration() {
return SystemClock.elapsedRealtime() - this.lastDiscoStarted;
}
public long getLastPingSent() {
return this.lastPingSent;
}
public long getLastDiscoStarted() {
return this.lastDiscoStarted;
}
public long getLastPacketReceived() {
return this.lastPacketReceived;
}
@ -2857,7 +2850,7 @@ public class XmppConnection implements Runnable {
public void resetAttemptCount(boolean resetConnectTime) {
this.attempt = 0;
if (resetConnectTime) {
this.lastConnect = 0;
this.lastConnectionStarted = 0;
}
}
@ -2905,6 +2898,22 @@ public class XmppConnection implements Runnable {
sendIqPacket(iqPacket, unregisteredIqListener);
}
public void triggerConnectionTimeout() {
final var duration = getConnectionDuration();
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": connection timeout after " + duration + "ms");
// last connection time gets reset so time to next attempt is calculated correctly
this.lastConnectionStarted = SystemClock.elapsedRealtime();
// interrupt needs to be called before status change; otherwise we interrupt the newly
// created thread
this.interrupt();
this.forceCloseSocket();
this.changeStatus(Account.State.CONNECTION_TIMEOUT);
}
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@ -3168,7 +3177,7 @@ public class XmppConnection implements Runnable {
new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
List<Entry<Jid, ServiceDiscoveryResult>> items =
findDiscoItemsByFeature(namespace);
if (items.size() > 0) {
if (!items.isEmpty()) {
try {
long maxsize =
Long.parseLong(
@ -3182,7 +3191,8 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": http upload is not available for files with size "
+ ": http upload is not available for files with"
+ " size "
+ filesize
+ " (max is "
+ maxsize
@ -3207,7 +3217,7 @@ public class XmppConnection implements Runnable {
for (String namespace :
new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
if (items.size() > 0) {
if (!items.isEmpty()) {
try {
return Long.parseLong(
items.get(0)

View file

@ -789,6 +789,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final var contact = account.getRoster().getContact(with);
callIntegration.setCallerDisplayName(
contact.getDisplayName(), TelecomManager.PRESENTATION_ALLOWED);
callIntegration.setInitialized();
callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
callIntegration.startAudioRouting();
final RtpSessionProposal proposal =

View file

@ -1,9 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
@ -16,7 +14,6 @@ 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.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Conversation;
@ -38,19 +35,8 @@ import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport;
import im.conversations.android.xmpp.model.jingle.Jingle;
import im.conversations.android.xmpp.model.stanza.Iq;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.io.CipherOutputStream;
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.webrtc.IceCandidate;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
@ -68,6 +54,14 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.io.CipherOutputStream;
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.webrtc.IceCandidate;
public class JingleFileTransferConnection extends AbstractJingleConnection
implements Transport.Callback, Transferable {
@ -205,8 +199,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
if (transition(
State.SESSION_INITIALIZED,
() -> this.initiatorFileTransferContentMap = contentMap)) {
final var iq =
contentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId);
final var iq = contentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId);
final var jingle = iq.getExtension(Jingle.class);
if (xmppAxolotlMessage != null) {
this.transportSecurity =
@ -456,8 +449,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
private void sendSessionAccept(final FileTransferContentMap contentMap) {
setLocalContentMap(contentMap);
final var iq =
contentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId);
final var iq = contentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId);
send(iq);
// this needs to come after session-accept or else our candidate-error might arrive first
this.transport.connect();
@ -562,7 +554,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
Log.d(Config.LOGTAG, "peer confirmed received " + received);
}
private void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) {
private synchronized void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) {
respondOk(jinglePacket);
final Jingle.ReasonWrapper wrapper = jingle.getReason();
final State previous = this.state;
@ -864,13 +856,21 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
@Override
public void onTransportEstablished() {
Log.d(Config.LOGTAG, "on transport established");
Log.d(Config.LOGTAG, "transport established");
final AbstractFileTransceiver fileTransceiver;
try {
fileTransceiver = setupTransceiver(isResponder());
} catch (final Exception e) {
Log.d(Config.LOGTAG, "failed to set up file transceiver", e);
sendSessionTerminate(Reason.ofThrowable(e), e.getMessage());
terminateTransport();
if (isTerminated()) {
Log.d(
Config.LOGTAG,
"failed to set up file transceiver but session has already been"
+ " terminated");
} else {
Log.d(Config.LOGTAG, "failed to set up file transceiver", e);
sendSessionTerminate(Reason.ofThrowable(e), e.getMessage());
}
return;
}
this.fileTransceiver = fileTransceiver;
@ -950,6 +950,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
}
private AbstractFileTransceiver setupTransceiver(final boolean receiving) throws IOException {
final var transport = this.transport;
if (transport == null) {
throw new IOException("No transport configured");
}
final var fileDescription = getLocalContentMap().requireOnlyFile();
final File file = xmppConnectionService.getFileBackend().getFile(message);
final Runnable updateRunnable = () -> jingleConnectionManager.updateConversationUi(false);
@ -986,7 +990,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) {
final var iq = new Iq(Iq.Type.SET);
final var jinglePacket = iq.addExtension(new Jingle(Jingle.Action.SESSION_INFO, this.id.sessionId));
final var jinglePacket =
iq.addExtension(new Jingle(Jingle.Action.SESSION_INFO, this.id.sessionId));
jinglePacket.addChild(sessionInfo.asElement());
send(iq);
}
@ -995,11 +1000,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
public void onTransportSetupFailed() {
final var transport = this.transport;
if (transport == null) {
// this can happen on IQ timeouts
if (isTerminated()) {
return;
synchronized (this) {
// this can happen on IQ timeouts
if (isTerminated()) {
return;
}
sendSessionTerminate(Reason.FAILED_APPLICATION, null);
}
sendSessionTerminate(Reason.FAILED_APPLICATION, null);
return;
}
Log.d(Config.LOGTAG, "onTransportSetupFailed");
@ -1070,8 +1077,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
+ contentName);
return;
}
final Iq iq =
transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
final Iq iq = transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
send(iq);
}
@ -1174,12 +1180,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
}
final var state = getState();
return switch (state) {
case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED -> Transferable
.STATUS_OFFER;
case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED ->
Transferable.STATUS_OFFER;
case TERMINATED_APPLICATION_FAILURE,
TERMINATED_CONNECTIVITY_ERROR,
TERMINATED_DECLINED_OR_BUSY,
TERMINATED_SECURITY_ERROR -> Transferable.STATUS_FAILED;
TERMINATED_SECURITY_ERROR ->
Transferable.STATUS_FAILED;
case TERMINATED_CANCEL_OR_TIMEOUT -> Transferable.STATUS_CANCELLED;
case SESSION_ACCEPTED -> Transferable.STATUS_DOWNLOADING;
default -> Transferable.STATUS_UNKNOWN;
@ -1254,7 +1261,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
}
terminateTransport();
final Iq iq = new Iq(Iq.Type.SET);
final var jingle = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
final var jingle =
iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
jingle.setReason(reason, "User requested to stop file transfer");
send(iq);
finish();

View file

@ -249,20 +249,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
receiveTransportInfo(jinglePacket, contentMap);
} else {
if (isTerminated()) {
respondOk(jinglePacket);
Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
+ ": ignoring out-of-order transport info; we where already terminated");
} else {
Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
+ ": received transport info while in state="
+ this.state);
terminateWithOutOfOrder(jinglePacket);
}
receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_INFO);
}
}
@ -350,7 +337,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
},
MoreExecutors.directExecutor());
} else {
terminateWithOutOfOrder(iq);
receiveOutOfOrderAction(iq, Jingle.Action.CONTENT_ADD);
}
}
@ -426,7 +413,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap outgoingContentAdd = this.outgoingContentAdd;
if (outgoingContentAdd == null) {
Log.d(Config.LOGTAG, "received content-accept when we had no outgoing content add");
terminateWithOutOfOrder(jinglePacket);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_ACCEPT);
return;
}
final Set<ContentAddition.Summary> ourSummary = ContentAddition.summary(outgoingContentAdd);
@ -456,7 +443,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
MoreExecutors.directExecutor());
} else {
Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add");
terminateWithOutOfOrder(jinglePacket);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_ACCEPT);
}
}
@ -497,7 +484,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveContentModify(final Iq jinglePacket, final Jingle jingle) {
if (this.state != State.SESSION_ACCEPTED) {
terminateWithOutOfOrder(jinglePacket);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_MODIFY);
return;
}
final Map<String, Content.Senders> modification =
@ -623,7 +610,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap outgoingContentAdd = this.outgoingContentAdd;
if (outgoingContentAdd == null) {
Log.d(Config.LOGTAG, "received content-reject when we had no outgoing content add");
terminateWithOutOfOrder(jinglePacket);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_REJECT);
return;
}
final Set<ContentAddition.Summary> ourSummary = ContentAddition.summary(outgoingContentAdd);
@ -634,7 +621,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
receiveContentReject(ourSummary);
} else {
Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add");
terminateWithOutOfOrder(jinglePacket);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_REJECT);
}
}
@ -1678,8 +1665,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
// in environments where we always use discovery timeouts we always want to respond with
// 'ringing'
if (Config.JINGLE_MESSAGE_INIT_STRICT_DEVICE_TIMEOUT
|| (xmppConnectionService.confirmMessages()
&& id.getContact().showInContactList())) {
|| id.getContact().showInContactList()) {
sendJingleMessage("ringing");
}
} else {

View file

@ -2,6 +2,7 @@ package eu.siacs.conversations.xmpp.jingle;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
import java.util.Arrays;
@ -9,6 +10,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Presence;
@ -25,14 +27,14 @@ public class RtpCapability {
Namespace.JINGLE_APPS_RTP,
Namespace.JINGLE_APPS_DTLS
);
private static final List<String> VIDEO_REQUIREMENTS = Arrays.asList(
private static final Collection<String> VIDEO_REQUIREMENTS = Arrays.asList(
Namespace.JINGLE_FEATURE_AUDIO,
Namespace.JINGLE_FEATURE_VIDEO
);
public static Capability check(final Presence presence) {
final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
final List<String> features = disco == null ? Collections.emptyList() : disco.getFeatures();
final Set<String> features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures());
if (features.containsAll(BASIC_RTP_REQUIREMENTS)) {
if (features.containsAll(VIDEO_REQUIREMENTS)) {
return Capability.VIDEO;
@ -66,7 +68,7 @@ public class RtpCapability {
public static Capability check(final Contact contact, final boolean allowFallback) {
final Presences presences = contact.getPresences();
if (presences.size() == 0 && allowFallback && contact.getAccount().isEnabled()) {
if (presences.isEmpty() && allowFallback && contact.getAccount().isEnabled()) {
Contact gateway = contact.getAccount().getRoster().getContact(Jid.of(contact.getJid().getDomain()));
if (gateway.showInRoster() && gateway.getPresences().anyIdentity("gateway", "pstn")) {
return Capability.AUDIO;

View file

@ -0,0 +1,16 @@
package im.conversations.android.xmpp.model.cb;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@XmlElement
public class ChannelBinding extends Extension {
public ChannelBinding() {
super(ChannelBinding.class);
}
public String getType() {
return this.getAttribute("type");
}
}

View file

@ -0,0 +1,25 @@
package im.conversations.android.xmpp.model.cb;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.StreamFeature;
import java.util.Collection;
@XmlElement
public class SaslChannelBinding extends StreamFeature {
public SaslChannelBinding() {
super(SaslChannelBinding.class);
}
public Collection<ChannelBinding> getChannelBindings() {
return this.getExtensions(ChannelBinding.class);
}
public Collection<String> getChannelBindingTypes() {
return Collections2.filter(
Collections2.transform(getChannelBindings(), ChannelBinding::getType),
Predicates.notNull());
}
}

View file

@ -0,0 +1,5 @@
@XmlPackage(namespace = Namespace.CHANNEL_BINDING)
package im.conversations.android.xmpp.model.cb;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlPackage;

View file

@ -14,6 +14,11 @@ public class Replace extends Extension {
super(Replace.class);
}
public Replace(final String id) {
this();
this.setId(id);
}
public String getId() {
return Strings.emptyToNull(this.getAttribute("id"));
}

View file

@ -0,0 +1,12 @@
package im.conversations.android.xmpp.model.streams;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.StreamElement;
@XmlElement(name = "error")
public class StreamError extends StreamElement {
public StreamError() {
super(StreamError.class);
}
}

View file

@ -1,5 +1,6 @@
package im.conversations.android.xmpp.model.unique;
import com.google.common.base.Strings;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@ -9,4 +10,13 @@ public class OriginId extends Extension {
public OriginId() {
super(OriginId.class);
}
public OriginId(final String id) {
this();
this.setAttribute("id", id);
}
public String getId() {
return Strings.emptyToNull(this.getAttribute("id"));
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z" />
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="7dp" android:top="0dp">
<item android:left="@dimen/bubble_avatar_distance" android:top="0dp">
<shape android:shape="rectangle">
<corners
android:radius="0dp"
@ -17,4 +17,16 @@
<solid android:color="?colorSurfaceContainerHigh" />
</shape>
</item>
<item android:gravity="top|left" android:width="@dimen/bubble_avatar_distance" android:height="@dimen/bubble_avatar_distance" android:top="0dp">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M24,0 V 24 L0,0 z"
android:strokeColor="?colorSurfaceContainerHigh"
android:fillColor="?colorSurfaceContainerHigh"/>
</vector>
</item>
</layer-list>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:right="7dp" android:bottom="0dp">
<item android:right="@dimen/bubble_avatar_distance" android:bottom="0dp">
<shape android:shape="rectangle">
<corners
android:radius="0dp"
@ -17,4 +17,16 @@
<solid android:color="?colorSurface" />
</shape>
</item>
<item android:bottom="0dp" android:gravity="bottom|right" android:width="@dimen/bubble_avatar_distance" android:height="@dimen/bubble_avatar_distance">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M0,24 V 0 L24,24 z"
android:strokeColor="?colorSurface"
android:fillColor="?colorSurface"/>
</vector>
</item>
</layer-list>

View file

@ -79,6 +79,41 @@
android:layout_above="@+id/button_row"
android:layout_below="@id/app_bar_layout">
<com.google.android.material.card.MaterialCardView
android:id="@+id/support_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="@dimen/rtp_session_duration_top_margin"
android:visibility="gone"
app:cardBackgroundColor="?colorErrorContainer">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp">
<ImageView
android:id="@+id/no_support_av_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@drawable/ic_warning_48dp"
app:tint="?colorOnErrorContainer" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_toEndOf="@+id/no_support_av_icon"
android:text="@string/clients_may_not_support_av"
android:textAppearance="?textAppearanceBodyLarge"
android:textColor="?colorOnErrorContainer" />
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"

View file

@ -83,7 +83,7 @@
android:stackFromBottom="true"
android:transcriptMode="normal"
android:layout_below="@+id/muc_subject"
tools:listitem="@layout/item_message_sent" />
tools:listitem="@layout/item_message_end" />
<LinearLayout
android:id="@+id/input_area"

View file

@ -66,6 +66,7 @@
android:autoLink="web"
android:maxLines="8"
android:longClickable="false"
android:breakStrategy="simple"
android:textAppearance="?textAppearanceBodyMedium"
android:visibility="gone" />

View file

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.daimajia.swipe.SwipeLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:drag_edge="left"
app:show_mode="pull_out"
android:id="@+id/layout_swipe"
android:orientation="vertical" >
<RelativeLayout
android:id="@+id/bottom_wrapper"
android:layout_width="80sp"
android:weightSum="1"
android:layout_height="match_parent"
android:orientation="horizontal"
android:clickable="true"
android:focusable="true" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/bubble_horizontal_padding"
android:paddingVertical="@dimen/bubble_vertical_padding">
<eu.siacs.conversations.ui.widget.AvatarView
android:id="@+id/message_photo"
android:layout_width="@dimen/bubble_avatar_size"
android:layout_height="@dimen/bubble_avatar_size"
app:layout_constraintBottom_toBottomOf="@id/message_box"
app:layout_constraintEnd_toEndOf="parent"
android:scaleType="centerCrop"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Photo" />
<LinearLayout
android:id="@+id/message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="6dp"
android:background="@drawable/message_bubble_sent"
android:backgroundTint="?colorSecondaryContainer"
android:longClickable="true"
android:minHeight="@dimen/bubble_avatar_size"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/message_photo"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/message_box_inner"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<include
android:id="@+id/message_content"
layout="@layout/item_message_content" />
<com.wefika.flowlayout.FlowLayout
android:id="@+id/status_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:gravity="end"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/message_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="2sp"
android:accessibilityTraversalAfter="@id/message_photo"
android:accessibilityTraversalBefore="@id/message_time"
android:gravity="center_vertical"
android:textAppearance="?textAppearanceLabelSmall"
android:textColor="?colorOnSecondaryContainer"
android:visibility="gone" />
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="2sp"
android:layout_marginEnd="2sp"
android:accessibilityTraversalAfter="@id/message_photo"
android:accessibilityTraversalBefore="@id/message_content"
android:gravity="center_vertical"
android:text="@string/sending"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnSecondaryContainer" />
<com.lelloman.identicon.view.GithubIdenticonView
android:id="@+id/thread_identicon"
android:background="@drawable/ic_thread"
android:visibility="gone"
android:layout_width="9dp"
android:layout_height="9dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4sp"
android:layout_marginBottom="-1dp" />
<ImageView
android:id="@+id/security_indicator"
android:layout_width="12sp"
android:layout_height="12sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4sp"
android:gravity="center_vertical"
android:src="@drawable/ic_lock_24dp"
app:tint="?colorOnSecondaryContainer" />
<ImageView
android:id="@+id/edit_indicator"
android:layout_width="12sp"
android:layout_height="12sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4sp"
android:gravity="center_vertical"
android:src="@drawable/ic_edit_24dp"
app:tint="?colorOnSecondaryContainer" />
<ImageView
android:id="@+id/indicator_received"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4sp"
android:gravity="center_vertical"
android:src="@drawable/ic_done_24dp"
app:tint="?colorOnSecondaryContainer" />
</com.wefika.flowlayout.FlowLayout>
</LinearLayout>
</LinearLayout>
<Space
android:id="@+id/reactions_anchor"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="@+id/message_box"
app:layout_constraintEnd_toEndOf="@+id/message_box" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="7dp"
android:orientation="horizontal"
android:visibility="visible"
app:chipSpacingHorizontal="2dp"
app:chipSpacingVertical="4dp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="@+id/message_box"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/reactions_anchor" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.daimajia.swipe.SwipeLayout>
</layout>

View file

@ -0,0 +1,189 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.daimajia.swipe.SwipeLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:drag_edge="left"
app:show_mode="pull_out"
android:id="@+id/layout_swipe"
android:orientation="vertical" >
<RelativeLayout
android:id="@+id/bottom_wrapper"
android:layout_width="80sp"
android:weightSum="1"
android:layout_height="match_parent"
android:orientation="horizontal"
android:clickable="true"
android:focusable="true" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/bubble_horizontal_padding"
android:paddingVertical="@dimen/bubble_vertical_padding">
<eu.siacs.conversations.ui.widget.AvatarView
android:id="@+id/message_photo"
android:layout_width="@dimen/bubble_avatar_size"
android:layout_height="@dimen/bubble_avatar_size"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/message_box"
android:layout_marginEnd="0dp"
android:scaleType="centerCrop"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Photo" />
<!-- TODO port app:layout_constraintWidth_max="@dimen/message_bubble_max_width" from c3 -->
<LinearLayout
android:id="@+id/message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dp"
android:background="@drawable/message_bubble_received"
android:backgroundTint="?colorTertiaryContainer"
android:longClickable="true"
android:minHeight="@dimen/bubble_avatar_size"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/message_photo"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/message_box_inner"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<include
android:id="@+id/message_content"
layout="@layout/item_message_content" />
<com.wefika.flowlayout.FlowLayout
android:id="@+id/status_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/message_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="4sp"
android:accessibilityTraversalAfter="@id/message_photo"
android:accessibilityTraversalBefore="@id/message_time"
android:gravity="center_vertical"
android:textAppearance="?textAppearanceLabelSmall"
android:textColor="?colorOnTertiaryContainer"
android:visibility="gone" />
<TextView
android:id="@+id/message_encryption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4sp"
android:gravity="center_vertical"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnTertiaryContainer"
tools:text="@string/not_trusted" />
<ImageView
android:id="@+id/security_indicator"
android:layout_width="12sp"
android:layout_height="12sp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4sp"
android:gravity="center_vertical"
android:src="@drawable/ic_lock_24dp"
app:tint="?colorOnTertiaryContainer" />
<ImageView
android:id="@+id/edit_indicator"
android:layout_width="12sp"
android:layout_height="12sp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4sp"
android:gravity="center_vertical"
android:src="@drawable/ic_edit_24dp"
app:tint="?colorOnTertiaryContainer" />
<ImageView
android:id="@+id/indicator_received"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4sp"
android:gravity="center_vertical"
android:src="@drawable/ic_done_24dp"
app:tint="?colorOnTertiaryContainer" />
<com.lelloman.identicon.view.GithubIdenticonView
android:id="@+id/thread_identicon"
android:background="@drawable/ic_thread"
android:visibility="gone"
android:layout_width="9dp"
android:layout_height="9dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4sp"
android:layout_marginBottom="-1dp" />
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:accessibilityTraversalAfter="@id/message_photo"
android:accessibilityTraversalBefore="@id/message_content"
android:gravity="center_vertical"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnTertiaryContainer"
tools:text="10:42" />
<TextView
android:id="@+id/message_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:visibility="gone"
android:textStyle="bold"
android:textSize="12sp"
android:textAppearance="?textAppearanceBodySmall" />
</com.wefika.flowlayout.FlowLayout>
</LinearLayout>
</LinearLayout>
<Space
android:id="@+id/reactions_anchor"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="@+id/message_box"
app:layout_constraintStart_toStartOf="@+id/message_box" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:orientation="horizontal"
android:visibility="visible"
app:chipSpacingHorizontal="2dp"
app:chipSpacingVertical="4dp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/message_box"
app:layout_constraintTop_toBottomOf="@+id/reactions_anchor" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.daimajia.swipe.SwipeLayout>
</layout>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_edit_contact"
@ -21,52 +21,58 @@
android:id="@+id/action_share"
android:icon="@drawable/ic_share_24dp"
android:orderInCategory="15"
app:showAsAction="always"
android:title="@string/share_uri_with">
android:title="@string/share_uri_with"
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_share_uri"
android:title="@string/share_as_uri"/>
android:title="@string/share_as_uri" />
<item
android:id="@+id/action_share_http"
android:title="@string/share_as_http"/>
android:title="@string/share_as_http" />
<item
android:id="@+id/action_show_qr_code"
android:title="@string/show_qr_code"/>
android:title="@string/show_qr_code" />
</menu>
</item>
<item
android:id="@+id/action_delete_contact"
android:orderInCategory="10"
app:showAsAction="never"
android:title="@string/action_delete_contact"/>
android:title="@string/action_delete_contact"
app:showAsAction="never" />
<item
android:id="@+id/action_block"
android:orderInCategory="72"
app:showAsAction="never"
android:title="@string/action_block_contact"/>
android:title="@string/action_block_contact"
app:showAsAction="never" />
<item
android:id="@+id/action_unblock"
android:orderInCategory="73"
app:showAsAction="never"
android:title="@string/action_unblock_contact"/>
android:title="@string/action_unblock_contact"
app:showAsAction="never" />
<item
android:id="@+id/action_custom_notifications"
android:orderInCategory="75"
android:title="@string/custom_notifications"
app:showAsAction="never" />
<item
android:id="@+id/action_accounts"
android:orderInCategory="90"
app:showAsAction="never"
android:title="@string/action_accounts"/>
android:title="@string/action_accounts"
app:showAsAction="never" />
<item
android:id="@+id/action_account"
android:orderInCategory="90"
android:title="@string/action_account"
app:showAsAction="never"/>
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
app:showAsAction="never"
android:title="@string/action_settings"/>
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

View file

@ -26,6 +26,11 @@
android:title="@string/copy_to_clipboard"
android:visible="false" />
<item
android:id="@+id/copy_link"
android:title="@string/copy_link"
android:visible="false" />
<item
android:id="@+id/quote_message"
android:title="@string/reply"

View file

@ -18,4 +18,5 @@
<bool name="allow_unencrypted_reactions">false</bool>
<bool name="swipe_to_archive">true</bool>
<bool name="default_store_media_in_cache">false</bool>
<bool name="is_portrait_mode">true</bool>
</resources>

View file

@ -66,5 +66,6 @@
<integer name="automatic_backup">0</integer>
<bool name="call_integration">true</bool>
<bool name="default_nomedia">true</bool>
<bool name="show_muc_pm">false</bool>
<bool name="show_muc_pm">true</bool>
<bool name="align_start">false</bool>
</resources>

View file

@ -90,16 +90,14 @@
<dimen name="colorpicker_hue_width">30dp</dimen>
<!--Lockscreen-->
<dimen name="forgot_password_margin_bottom">13.6dp</dimen>
<dimen name="number_pad_margin_top">7.3dp</dimen>
<dimen name="number_pad_parent_margin">14.5dp</dimen>
<!-- bubble avatar -->
<dimen name="bubble_horizontal_padding">8dp</dimen>
<dimen name="bubble_vertical_padding">4dp</dimen>
<dimen name="bubble_vertical_padding_minimum">0dp</dimen>
<dimen name="bubble_vertical_padding_minimum">1dp</dimen>
<dimen name="bubble_avatar_size">48dp</dimen>
<dimen name="bubble_avatar_distance">2dp</dimen>
<dimen name="bubble_avatar_distance">6dp</dimen>
</resources>

View file

@ -159,6 +159,7 @@
<string name="account_status_unauthorized">Unauthorized</string>
<string name="account_status_not_found">Server not found</string>
<string name="account_status_no_internet">No connectivity</string>
<string name="account_status_connection_timeout">Connection timeout</string>
<string name="account_status_regis_fail">Registration failed</string>
<string name="account_status_regis_conflict">Username already in use</string>
<string name="account_status_regis_success">Registration completed</string>
@ -469,7 +470,7 @@
<string name="download_failed_invalid_file">Download failed: Invalid file</string>
<string name="account_status_tor_unavailable">Tor network unavailable</string>
<string name="account_status_bind_failure">Bind failure</string>
<string name="account_status_host_unknown">The server is not responsible for this domain</string>
<string name="account_status_host_unknown">Not responsible for domain</string>
<string name="server_info_broken">Broken</string>
<string name="pref_presence_settings">Availability</string>
<string name="pref_away_when_screen_off">Away when device is locked</string>
@ -1429,4 +1430,9 @@
<string name="create_monocles_account">Sign up</string>
<string name="or_use_another">… or choose another one below</string>
<string name="use_monocles">I prefer monocles</string>
<string name="clients_may_not_support_av">Your contacts XMPP client might not support audio/video calls.</string>
<string name="pref_align_start">Left-aligned messages</string>
<string name="pref_align_start_summary">Display all messages, including sent ones, on the left side for a uniform chat layout.</string>
<string name="custom_notifications">Custom notifications</string>
<string name="custom_notifications_enable">Enable customized notification settings (importance, sound, vibration) settings for this conversation?</string>
</resources>

View file

@ -79,7 +79,7 @@
android:title="@string/custom_background_color"
android:defaultValue="@color/md_theme_dark_surface"
app:colorpicker_showAlpha="false" />
</PreferenceCategory>>
</PreferenceCategory>
<PreferenceCategory android:title="@string/appearance">
<Preference
android:key="import_background"

View file

@ -6,6 +6,11 @@
android:key="use_green_background"
android:summary="@string/pref_use_colorful_bubbles_summary"
android:title="@string/pref_use_colorful_bubbles" />
<SwitchPreferenceCompat
android:icon="@drawable/ic_format_align_left_24dp"
android:key="align_start"
android:summary="@string/pref_align_start_summary"
android:title="@string/pref_align_start" />
<SwitchPreferenceCompat
android:defaultValue="@bool/show_avatars"
android:icon="@drawable/ic_account_circle_24dp"