Merge remote-tracking branch 'origin/master'

This commit is contained in:
12aw 2022-04-29 01:33:25 +02:00
commit e1d59a79f2
40 changed files with 621 additions and 180 deletions

View file

@ -21,13 +21,27 @@ The changes aim to improve usability and ease transition from pre-installed and
<img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/00.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/01.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/02.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/03.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/04.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/05.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/06.png" width="200" /> <img src="https://codeberg.org/Arne/monocles_chat/raw/branch/master/fastlane/metadata/android/en-US/phoneScreenshots/07.png" width="200" />
### Presettings
monocles chat has different presettings compared to blabber.im:
* don't show previews of weblinks in chat
* don't show previews of locations in chat
* use inner storage (files are hidden then and not shown in the Gallery)
* don't automatically download all atachments
### OTR
monocles chat supports OTR encryption! Though it's not easy to use OTR does have some advantages:
<a href="https://en.wikipedia.org/wiki/Off-the-Record_Messaging#Implementation">https://en.wikipedia.org/wiki/Off-the-Record_Messaging#Implementation</a>
## Download
monocles chat is available for install in the F-Droid
Alternatively release and beta-release APKs are available via codeberg: [Releases](https://codeberg.org/Arne/monocles_chat/releases/latest)
#### monocles chat nightly and beta
nightly or beta-release APKs are available via codeberg: [Releases](https://codeberg.org/Arne/monocles_chat/releases/nightly)
nightly or beta-release APKs are available via codeberg: [Releases](https://codeberg.org/Arne/monocles_chat/releases)
## Social Media
Follow us on <a rel="me" href="https://monocles.social/@monocles">monocles social</a>

View file

@ -34,13 +34,12 @@ configurations {
}
dependencies {
playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
playstoreImplementation('com.google.firebase:firebase-messaging:23.0.2') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
playstoreImplementation 'com.android.installreferrer:installreferrer:2.2'
playstoreImplementation 'com.google.gms:google-services:4.3.8'
implementation 'org.sufficientlysecure:openpgp-api:10.0'
implementation('com.theartofdev.edmodo:android-image-cropper:2.8.0') {
exclude group: 'com.android.support', module: 'appcompat-v7'
@ -86,10 +85,10 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.guava:guava:31.0.1-android'
implementation 'com.github.AppIntro:AppIntro:6.1.0'
implementation 'androidx.browser:browser:1.3.0' // 1.4.0 needs minCompileSdk 31
implementation 'androidx.browser:browser:1.4.0'
implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy
implementation project(':libs:AXML')
implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs')
implementation fileTree(include: ['libwebrtc-m99.aar'], dir: 'libs')
}
ext {
@ -107,8 +106,8 @@ android {
targetSdkVersion 30
//versionNameSuffix " beta_(2021-12-19)" // " beta_(XXXX-XX-XX)" // activate for beta versions
versionCode 112
versionName "1.5.1"
versionCode 113
versionName "1.5.2"
//resConfigs "en"
archivesBaseName += "-$versionName"

View file

@ -11,9 +11,9 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 112,
"versionName": "1.5.1",
"outputFile": "monocles chat-1.5.1null-git-release.apk"
"versionCode": 113,
"versionName": "1.5.2",
"outputFile": "monocles chat-1.5.2null-git-release.apk"
}
],
"elementType": "File"

View file

@ -112,19 +112,16 @@ public class StartUI extends PermissionsActivity
@Override
protected Void doInBackground(Void... params) {
DatabaseBackend mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
Log.d(Config.LOGTAG, "Optimizing database");
final Stopwatch stopwatch = Stopwatch.createStarted();
try {
try (DatabaseBackend mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext())) {
Log.d(Config.LOGTAG, "Optimizing database");
final Stopwatch stopwatch = Stopwatch.createStarted();
final SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
db.execSQL("ANALYZE");
db.execSQL("VACUUM");
//db.execSQL("VACUUM"); // todo should we do it?
db.execSQL("PRAGMA optimize");
Log.d(Config.LOGTAG, String.format("Optimized database in %s", stopwatch.stop()));
} catch (Exception e) {
e.printStackTrace();
} finally {
mDatabaseBackend.close();
}
return null;
}

View file

@ -64,6 +64,12 @@
<!-- OpenKeyChain -->
<package android:name="org.sufficientlysecure.keychain"/>
<intent>
<action android:name="eu.siacs.conversations.location.request"/>
</intent>
<intent>
<action android:name="eu.siacs.conversations.location.show"/>
</intent>
</queries>

View file

@ -109,7 +109,7 @@ public final class Config {
public static final boolean XEP_0392 = true; //enables XEP-0392 v0.6.0
public static final int VIDEO_FAST_UPLOAD_SIZE = 5 * 1024 * 1024;
public static final int VIDEO_FAST_UPLOAD_SIZE = 10 * 1024 * 1024;
public static final int AVATAR_SIZE = 480;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG;

View file

@ -22,6 +22,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.http.URL;
import eu.siacs.conversations.services.AvatarService;
@ -1034,7 +1035,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public boolean isTrusted() {
FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
return s != null && s.isTrusted();
}

View file

@ -180,6 +180,7 @@ public class IqGenerator extends AbstractGenerator {
info.setAttribute("type", avatar.type);
return publish("urn:xmpp:avatar:metadata", item, options);
}
public IqPacket deleteAvatar() {
final Element item = new Element("item");
item.addChild("metadata", "urn:xmpp:avatar:metadata");
@ -461,16 +462,20 @@ public class IqGenerator extends AbstractGenerator {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(appServer);
final Element command = packet.addChild("command", Namespace.COMMANDS);
command.setAttribute("node", "register-push-fcm");
command.setAttribute("node", "v1-register-push");
command.setAttribute("action", "execute");
final Data data = new Data();
data.put("type", "fcm");
data.put("token", token);
data.put("android-id", deviceId);
if (muc != null) {
data.put("node", deviceId);
data.put("FORM_TYPE", "https://github.com/tmolitor-stud-tu/mod_push_appserver/#v1-register-push");
// to do MUC not support
/*if (muc != null) {
data.put("muc", muc.toEscapedString());
}
}*/
data.submit();
command.addChild(data);
Log.d(Config.LOGTAG, "Push packet " + packet);
return packet;
}
@ -481,8 +486,11 @@ public class IqGenerator extends AbstractGenerator {
command.setAttribute("node", "unregister-push-fcm");
command.setAttribute("action", "execute");
final Data data = new Data();
data.put("type", "fcm");
data.put("node", deviceId);
data.put("channel", channel);
data.put("android-id", deviceId);
data.put("FORM_TYPE", "https://github.com/tmolitor-stud-tu/mod_push_appserver/#v1-unregister-push");
data.submit();
command.addChild(data);
return packet;

View file

@ -117,11 +117,20 @@ public class HttpDownloadConnection implements Transferable {
if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
//TODO add auth tag size to knownFileSize
final Long knownFileSize = message.getFileParams().size;
final Long knownFileSize;
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
knownFileSize = null;
} else {
knownFileSize = message.getFileParams().size;
}
Log.d(Config.LOGTAG, "knownFileSize: " + knownFileSize + ", body=" + message.getBody());
if (knownFileSize != null && interactive) {
this.file.setExpectedSize(knownFileSize);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL
&& this.file.getKey() != null) {
this.file.setExpectedSize(knownFileSize + 16);
} else {
this.file.setExpectedSize(knownFileSize);
}
download(true);
} else {
checkFileSize(interactive);
@ -235,6 +244,8 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
} else if (e instanceof FileWriterException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
} else if (e instanceof InvalidFileException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_invalid_file);
} else {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
}
@ -330,6 +341,7 @@ public class HttpDownloadConnection implements Transferable {
);
final Request request = new Request.Builder()
.url(URL.stripFragment(mUrl))
.addHeader("Accept-Encoding", "identity")
.head()
.build();
mostRecentCall = client.newCall(request);
@ -355,11 +367,11 @@ public class HttpDownloadConnection implements Transferable {
throw new IOException("Server reported negative file size");
}
return size;
} catch (IOException e) {
} catch (final IOException e) {
Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
throw e;
} catch (NumberFormatException e) {
throw new IOException();
} catch (final NumberFormatException e) {
throw new IOException(e);
}
}
@ -448,9 +460,12 @@ public class HttpDownloadConnection implements Transferable {
transmitted += count;
try {
outputStream.write(buffer, 0, count);
} catch (IOException e) {
} catch (final IOException e) {
throw new FileWriterException(file);
}
if (transmitted > expected) {
throw new InvalidFileException(String.format("File exceeds expected size of %d", expected));
}
updateProgress(Math.round(((double) transmitted / expected) * 100));
}
outputStream.flush();
@ -478,4 +493,12 @@ public class HttpDownloadConnection implements Transferable {
throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code));
}
}
private static class InvalidFileException extends IOException {
private InvalidFileException(final String message) {
super(message);
}
}
}

View file

@ -14,6 +14,15 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import android.os.Build;
import android.text.Html;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@ -52,6 +61,8 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian");
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract");
@ -96,6 +107,31 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return result != null ? result : fallback;
}
private static boolean clientMightSendHtml(Account account, Jid from) {
String resource = from.getResource();
if (resource == null) {
return false;
}
Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource);
ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
if (disco == null) {
return false;
}
return hasIdentityKnowForSendingHtml(disco.getIdentities());
}
private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
for (ServiceDiscoveryResult.Identity identity : identities) {
if (identity.getName() != null) {
if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
return true;
}
}
}
return false;
}
private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
ChatState state = ChatState.parse(packet);
if (state != null && c != null) {
@ -127,6 +163,67 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return false;
}
private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) {
String presence;
if (from.isBareJid()) {
presence = "";
} else {
presence = from.getResource();
}
if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) {
conversation.endOtrIfNeeded();
}
if (!conversation.hasValidOtrSession()) {
conversation.startOtrSession(presence, false);
} else {
String foreignPresence = conversation.getOtrSession().getSessionID().getUserID();
if (!foreignPresence.equals(presence)) {
conversation.endOtrIfNeeded();
conversation.startOtrSession(presence, false);
}
}
try {
conversation.setLastReceivedOtrMessageId(id);
Session otrSession = conversation.getOtrSession();
body = otrSession.transformReceiving(body);
SessionStatus status = otrSession.getSessionStatus();
if (body == null && status == SessionStatus.ENCRYPTED) {
mXmppConnectionService.onOtrSessionEstablished(conversation);
return null;
} else if (body == null && status == SessionStatus.FINISHED) {
conversation.resetOtrSession();
mXmppConnectionService.updateConversationUi();
return null;
} else if (body == null || (body.isEmpty())) {
return null;
}
if (body.startsWith(CryptoHelper.FILETRANSFER)) {
String key = body.substring(CryptoHelper.FILETRANSFER.length());
conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
return null;
}
if (clientMightSendHtml(conversation.getAccount(), from)) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString();
} else {
body = Html.fromHtml(body).toString();
}
}
final OtrService otrService = conversation.getAccount().getOtrService();
Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
conversation.setLastReceivedOtrMessageId(null);
return finishedMessage;
} catch (Exception e) {
conversation.resetOtrSession();
return null;
}
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
final AxolotlService service = conversation.getAccount().getAxolotlService();
final XmppAxolotlMessage xmppAxolotlMessage;
@ -354,6 +451,15 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
}
if (message != null) {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
Conversation conversation = (Conversation) message.getConversation();
conversation.endOtrIfNeeded();
}
}
}
return true;
}
@ -521,7 +627,20 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
final Message message;
if (pgpEncrypted != null && Config.supportOpenPgp()) {
if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) {
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
message = parseOtrChat(body.content, from, remoteMsgId, conversation);
if (message == null) {
return;
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
if (body.count > 1) {
message.setBodyLanguage(body.language);
}
}
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
Jid origin;
@ -835,6 +954,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
processMessageReceipts(account, packet, remoteMsgId, query);
}
if (message.getStatus() == Message.STATUS_RECEIVED
&& conversation.getOtrSession() != null
&& !conversation.getOtrSession().getSessionID().getUserID()
.equals(message.getCounterpart().getResource())) {
conversation.endOtrIfNeeded();
}
mXmppConnectionService.databaseBackend.createMessage(message);
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
if ((mXmppConnectionService.easyDownloader() || message.trusted()) && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {

View file

@ -746,7 +746,7 @@ public class FileBackend {
if (cursor != null && cursor.moveToFirst()) {
filename = cursor.getString(0);
}
} catch (final SecurityException | IllegalArgumentException e) {
} catch (final Exception e) {
filename = null;
}
if (filename == null) {

View file

@ -43,6 +43,7 @@ import androidx.appcompat.app.AppCompatActivity;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import org.json.JSONArray;
@ -77,6 +78,7 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.entities.MTMDecision;
@ -391,13 +393,13 @@ public class MemorizingTrustManager {
final List<String> fingerprints = getPoshFingerprints(domain);
if (hash != null && fingerprints.size() > 0) {
if (fingerprints.contains(hash)) {
Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh");
Log.d(Config.LOGTAG, "trusted cert fingerprint of " + domain + " via posh");
return;
} else {
Log.d("mtm", "fingerprint " + hash + " not found in " + fingerprints);
Log.d(Config.LOGTAG, "fingerprint " + hash + " not found in " + fingerprints);
}
if (getPoshCacheFile(domain).delete()) {
Log.d("mtm", "deleted posh file for " + domain + " after not being able to verify");
Log.d(Config.LOGTAG, "deleted posh file for " + domain + " after not being able to verify");
}
}
}
@ -410,7 +412,7 @@ public class MemorizingTrustManager {
}
}
private List<String> getPoshFingerprints(String domain) {
private List<String> getPoshFingerprints(final String domain) {
final List<String> cached = getPoshFingerprintsFromCache(domain);
if (cached == null) {
return getPoshFingerprintsFromServer(domain);
@ -424,13 +426,13 @@ public class MemorizingTrustManager {
}
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
Log.d("mtm", "downloading json for " + domain + " from " + url);
Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor));
try {
final List<String> results = new ArrayList<>();
final InputStream inputStream = HttpConnectionManager.open(url, useTor);
final String body = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream,10_000), Charsets.UTF_8));
final JSONObject jsonObject = new JSONObject(body);
int expires = jsonObject.getInt("expires");
if (expires <= 0) {
@ -457,7 +459,7 @@ public class MemorizingTrustManager {
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
return results;
} catch (final Exception e) {
Log.d("mtm", "error fetching posh " + e.getMessage());
Log.d(Config.LOGTAG, "error fetching posh",e);
return new ArrayList<>();
}
}
@ -495,7 +497,7 @@ public class MemorizingTrustManager {
file.delete();
return null;
} else {
Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s");
Log.d(Config.LOGTAG, "posh fingerprints expire in " + (expiresIn / 1000) + "s");
}
final List<String> result = new ArrayList<>();
final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
@ -512,7 +514,6 @@ public class MemorizingTrustManager {
}
private X509Certificate[] getAcceptedIssuers() {
LOGGER.log(Level.FINE, "getAcceptedIssuers()");
return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers();
}

View file

@ -258,7 +258,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
//do nothing
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString());
finalizeQuery(query, true);
try {
finalizeQuery(query, true);
} catch (final IllegalStateException e) {
//ignored
}
}
});
} else {

View file

@ -98,6 +98,8 @@ public class NotificationService {
private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages";
private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls";
private static final int MESSAGE_DAT = 70;
private static final long[] MESSAGE_PATTERN = {0, 3 * MESSAGE_DAT, MESSAGE_DAT, MESSAGE_DAT};
private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
public static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER;
public static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
@ -109,6 +111,7 @@ public class NotificationService {
public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 14;
public static final int IMPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 16;
public static final int EXPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 18;
public static final int UPDATE_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 20;
private final XmppConnectionService mXmppConnectionService;
private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
private final LinkedHashMap<Conversational, MissedCallsInfo> mMissedCalls = new LinkedHashMap<>();
@ -268,14 +271,18 @@ public class NotificationService {
// create individual notification channels for selected chats
@RequiresApi(api = Build.VERSION_CODES.O)
private void createIndividualNotificationChannels(final NotificationManager notificationManager) {
final int chats = mXmppConnectionService.getConversations().size();
for (int i = 0; i < chats; i++) {
if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.getConversations().get(i))) {
if (mXmppConnectionService.getConversations().get(i).getMode() == Conversation.MODE_SINGLE) {
createCallNotificationChannels(notificationManager, i);
try {
final int chats = mXmppConnectionService.getConversations().size();
for (int i = 0; i < chats; i++) {
if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.getConversations().get(i))) {
if (mXmppConnectionService.getConversations().get(i).getMode() == Conversation.MODE_SINGLE) {
createCallNotificationChannels(notificationManager, i);
}
createMessageNotificationChannels(notificationManager, i);
}
createMessageNotificationChannels(notificationManager, i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@ -327,9 +334,7 @@ public class NotificationService {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build());
messagesChannel.setLightColor(LED_COLOR);
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
messagesChannel.setVibrationPattern(pattern);
messagesChannel.setVibrationPattern(MESSAGE_PATTERN);
messagesChannel.enableVibration(true);
messagesChannel.enableLights(true);
notificationManager.createNotificationChannelGroup(new NotificationChannelGroup(INDIVIDUAL_NOTIFICATION_PREFIX + name + uuid, name + " (" + jid + ")"));
@ -354,9 +359,7 @@ public class NotificationService {
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build());
messagesChannel.setLightColor(LED_COLOR);
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
messagesChannel.setVibrationPattern(pattern);
messagesChannel.setVibrationPattern(MESSAGE_PATTERN);
messagesChannel.enableVibration(true);
messagesChannel.enableLights(true);
messagesChannel.setGroup("chats");
@ -463,16 +466,20 @@ public class NotificationService {
// clean all individual notification settings
@RequiresApi(api = Build.VERSION_CODES.O)
public void cleanAllNotificationChannels(final Context context) {
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
final int chats = mXmppConnectionService.getConversations().size();
for (int i = 0; i < chats; i++) {
if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.getConversations().get(i))) {
final String uuid = mXmppConnectionService.getConversations().get(i).getUuid();
mXmppConnectionService.setIndividualNotificationPreference(mXmppConnectionService.getConversations().get(i), true);
cleanCallNotificationChannels(notificationManager, uuid);
cleanMessageNotificationChannels(notificationManager, uuid);
cleanNotificationGroup(notificationManager, uuid);
try {
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
final int chats = mXmppConnectionService.getConversations().size();
for (int i = 0; i < chats; i++) {
if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.getConversations().get(i))) {
final String uuid = mXmppConnectionService.getConversations().get(i).getUuid();
mXmppConnectionService.setIndividualNotificationPreference(mXmppConnectionService.getConversations().get(i), true);
cleanCallNotificationChannels(notificationManager, uuid);
cleanMessageNotificationChannels(notificationManager, uuid);
cleanNotificationGroup(notificationManager, uuid);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@ -488,10 +495,6 @@ public class NotificationService {
notificationManager.deleteNotificationChannel(channelID);
}
}
} catch (Exception e) {
e.printStackTrace();
}
try {
final int groups = notificationManager.getNotificationChannelGroups().size();
for (int i2 = 0; i2 < groups; i2++) {
final String groupID = notificationManager.getNotificationChannelGroups().get(i2).getId();
@ -987,9 +990,7 @@ public class NotificationService {
final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications));
if (notify && !quietHours) {
if (vibrate) {
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
mBuilder.setVibrate(pattern);
mBuilder.setVibrate(MESSAGE_PATTERN);
} else {
mBuilder.setVibrate(new long[]{0});
}
@ -1256,7 +1257,7 @@ public class NotificationService {
mBuilder.addAction(snoozeAction);
++addedActionsCount;
}
if (addedActionsCount < 3 && mXmppConnectionService.webViewAvailable()) {
if (addedActionsCount < 3) {
final Message firstLocationMessage = getFirstLocationMessage(messages);
if (firstLocationMessage != null) {
final PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage);
@ -1772,7 +1773,7 @@ public class NotificationService {
}
public void AppUpdateServiceNotification(Notification notification) {
notify(FOREGROUND_NOTIFICATION_ID, notification);
notify(UPDATE_NOTIFICATION_ID, notification);
}
private void notify(String tag, int id, Notification notification) {

View file

@ -6,6 +6,7 @@ import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -76,12 +77,12 @@ public class ProviderService extends AsyncTask<String, Object, Boolean> {
String ratingC2S = null;
String ratingS2S = null;
int ratingXmppComplianceTester = 0;
final String provider = jsonObject.names().getString(i);
final String provider = Objects.requireNonNull(jsonObject.names()).getString(i);
if (provider.length() > 0) {
for (int ii = 0; ii < jsonObject.length(); ii++) {
final JSONObject json = new JSONObject(jsonObject.get(provider).toString());
String featureName = json.names().getString(ii);
final JSONObject subjson = new JSONObject(json.get(json.names().getString(ii)).toString());
final JSONObject json = new JSONObject(jsonObject.get(provider).toString());
for (int ii = 0; ii < json.length(); ii++) {
String featureName = Objects.requireNonNull(json.names()).getString(ii);
final JSONObject subjson = new JSONObject(json.get(Objects.requireNonNull(json.names()).getString(ii)).toString());
if (featureName.equals("inBandRegistration")) {
inBandRegistration = subjson.getBoolean("content");
}

View file

@ -17,6 +17,7 @@ import static eu.siacs.conversations.utils.RichPreview.RICH_LINK_METADATA;
import static eu.siacs.conversations.utils.StorageHelper.getAppMediaDirectory;
import android.Manifest;
import androidx.annotation.RequiresApi;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlarmManager;
@ -59,6 +60,13 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
import eu.siacs.conversations.xmpp.Jid;
import androidx.annotation.BoolRes;
import androidx.annotation.IntegerRes;
@ -259,9 +267,18 @@ public class XmppConnectionService extends Service {
Conversation conversation = find(getConversations(), contact);
if (conversation != null) {
if (online) {
conversation.endOtrIfNeeded();
if (contact.getPresences().size() == 1) {
sendUnsentMessages(conversation);
}
} else {
//check if the resource we are haveing a conversation with is still online
if (conversation.hasValidOtrSession()) {
String otrResource = conversation.getOtrSession().getSessionID().getUserID();
if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
conversation.endOtrIfNeeded();
}
}
}
}
};
@ -442,6 +459,9 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account
&& !pendingJoin
&& !inProgressJoin) {
if (!conversation.startOtrIfNeeded()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed");
}
sendUnsentMessages(conversation);
}
}
@ -1370,7 +1390,9 @@ public class XmppConnectionService extends Service {
@Override
public void onCreate() {
updateNotificationChannels();
cleanOldNotificationChannels();
if (Compatibility.runsTwentySix()) {
cleanOldNotificationChannels();
}
mChannelDiscoveryService.initializeMuclumbusService();
mForceDuringOnCreate.set(Compatibility.runsAndTargetsTwentySix(this));
toggleForegroundService();
@ -1471,7 +1493,7 @@ public class XmppConnectionService extends Service {
new Thread(mNotificationService::updateChannels).start();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void cleanOldNotificationChannels() {
new Thread(() -> {
try {
@ -1783,6 +1805,11 @@ public class XmppConnectionService extends Service {
databaseBackend.updateConversation(conversation);
}
}
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
conversation.endOtrIfNeeded();
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
message1 -> markMessage(message1, Message.STATUS_SEND_FAILED));
}
final boolean inProgressJoin = isJoinInProgress(conversation);
@ -1815,6 +1842,30 @@ public class XmppConnectionService extends Service {
packet = mMessageGenerator.generatePgpChat(message);
}
break;
case Message.ENCRYPTION_OTR:
SessionImpl otrSession = conversation.getOtrSession();
if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
try {
message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID()));
} catch (IllegalArgumentException e) {
break;
}
if (message.needsUploading()) {
mJingleConnectionManager.startJingleFileTransfer(message);
} else {
packet = mMessageGenerator.generateOtrChat(message);
}
} else if (otrSession == null) {
if (message.fixCounterpart()) {
conversation.startOtrSession(message.getCounterpart().getResource(), true);
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart());
break;
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString());
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
if (message.needsUploading()) {
@ -1867,6 +1918,12 @@ public class XmppConnectionService extends Service {
}
}
break;
case Message.ENCRYPTION_OTR:
if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid());
conversation.startOtrSession(message.getCounterpart().getResource(), false);
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
break;
@ -2093,7 +2150,9 @@ public class XmppConnectionService extends Service {
public void createBookmark(final Account account, final Bookmark bookmark) {
account.putBookmark(bookmark);
final XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().bookmarks2()) {
if (connection == null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": no connection. ignoring bookmark creation");
} else if (connection != null && connection.getFeatures().bookmarks2()) {
final Element item = mIqGenerator.publishBookmarkItem(bookmark);
pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems());
} else if (connection != null && connection.getFeatures().bookmarksConversion()) {
@ -2588,7 +2647,7 @@ public class XmppConnectionService extends Service {
getNotificationService().clear(conversation);
conversation.setStatus(Conversation.STATUS_ARCHIVED);
conversation.setNextMessage(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Compatibility.runsTwentySix()) {
try {
mNotificationService.cleanNotificationChannels(this, conversation.getUuid());
} catch (Exception e) {
@ -2776,7 +2835,7 @@ public class XmppConnectionService extends Service {
for (final Conversation conversation : conversations) {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Compatibility.runsTwentySix()) {
try {
mNotificationService.cleanNotificationChannels(this, conversation.getUuid());
} catch (Exception e) {
@ -3445,7 +3504,7 @@ public class XmppConnectionService extends Service {
}
private void leaveMuc(Conversation conversation, boolean now) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Compatibility.runsTwentySix()) {
try {
mNotificationService.cleanNotificationChannels(this, conversation.getUuid());
} catch (Exception e) {
@ -3771,6 +3830,12 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
leaveMuc(conversation, true);
} else {
if (conversation.endOtrIfNeeded()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()
+ ": ended otr session with "
+ conversation.getJid());
}
}
}
}
@ -3827,6 +3892,65 @@ public class XmppConnectionService extends Service {
}
pushContactToServer(contact, preAuth);
}
public void onOtrSessionEstablished(Conversation conversation) {
final Account account = conversation.getAccount();
final Session otrSession = conversation.getOtrSession();
Log.d(Config.LOGTAG,
account.getJid().asBareJid() + " otr session established with "
+ conversation.getJid() + "/"
+ otrSession.getSessionID().getUserID());
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
SessionID id = otrSession.getSessionID();
try {
message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID()));
} catch (IllegalArgumentException e) {
return;
}
if (message.needsUploading()) {
mJingleConnectionManager.startJingleFileTransfer(message);
} else {
MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
if (outPacket != null) {
mMessageGenerator.addDelay(outPacket, message.getTimeSent());
message.setStatus(Message.STATUS_SEND);
databaseBackend.updateMessage(message, false);
sendMessagePacket(account, outPacket);
}
}
updateConversationUi();
}
});
}
public boolean renewSymmetricKey(Conversation conversation) {
Account account = conversation.getAccount();
byte[] symmetricKey = new byte[32];
this.mRandom.nextBytes(symmetricKey);
Session otrSession = conversation.getOtrSession();
if (otrSession != null) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT);
packet.setFrom(account.getJid());
MessageGenerator.addMessageHints(packet);
packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
+ otrSession.getSessionID().getUserID());
try {
packet.setBody(otrSession
.transformSending(CryptoHelper.FILETRANSFER
+ CryptoHelper.bytesToHex(symmetricKey))[0]);
sendMessagePacket(account, packet);
conversation.setSymmetricKey(symmetricKey);
return true;
} catch (OtrException e) {
return false;
}
}
return false;
}
public void pushContactToServer(final Contact contact) {
pushContactToServer(contact, null);
@ -4547,11 +4671,15 @@ public class XmppConnectionService extends Service {
}
public void vibrate() {
final boolean vibrateInChat = getBooleanPreference("vibrate_in_chat", R.bool.vibrate_in_chat);
if (!isPhoneSilenced() && vibrateInChat) {
Log.d(Config.LOGTAG, "Notification: short vibrate");
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(100);
try {
final boolean vibrateInChat = getBooleanPreference("vibrate_in_chat", R.bool.vibrate_in_chat);
if (!isPhoneSilenced() && vibrateInChat) {
Log.d(Config.LOGTAG, "Notification: short vibrate");
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(100);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@ -5058,10 +5186,6 @@ public class XmppConnectionService extends Service {
}
}
public boolean webViewAvailable() {
return this.getPackageManager().hasSystemFeature("android.software.webview");
}
public void publishDisplayName(Account account) {
String displayName = account.getDisplayName();
final IqPacket request;
@ -5204,7 +5328,10 @@ public class XmppConnectionService extends Service {
boolean performedVerification = false;
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
for (XmppUri.Fingerprint fp : fingerprints) {
if (fp.type == XmppUri.FingerprintType.OMEMO) {
if (fp.type == XmppUri.FingerprintType.OTR) {
performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
needsRosterWrite |= performedVerification;
} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {

View file

@ -163,6 +163,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import me.drakeet.support.toast.ToastCompat;
import net.java.otr4j.session.SessionStatus;
public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
@ -212,6 +213,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private Toast messageLoaderToast;
private ConversationsActivity activity;
private Menu mOptionsMenu;
protected OnClickListener clickToVerify = new OnClickListener() {
@Override
public void onClick(View v) {
activity.verifyOtrSessionDialog(conversation, v);
}
};
private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm (z)", Locale.US);
@ -508,6 +515,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
};
private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(activity, VerifyOTRActivity.class);
intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
startActivity(intent);
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
}
};
protected OnClickListener clickToDecryptListener = new OnClickListener() {
@ -921,6 +940,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
message.setUuid(UUID.randomUUID().toString());
}
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_OTR:
sendOtrMessage(message);
break;
case Message.ENCRYPTION_PGP:
sendPgpMessage(message);
break;
@ -1051,7 +1073,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
break;
case REQUEST_INVITE_TO_CONVERSATION:
XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data);
if (invite != null) {
if (invite != null && activity != null) {
if (invite.execute(activity)) {
activity.mToast = ToastCompat.makeText(activity, R.string.creating_conference, ToastCompat.LENGTH_LONG);
activity.mToast.show();
@ -1427,7 +1449,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (ShareUtil.containsXmppUri(body)) {
copyLink.setTitle(R.string.copy_jabber_id);
copyLink.setVisible(true);
} else if (Patterns.WEB_URL.matcher(body).find()) {
} else if (Patterns.AUTOLINK_WEB_URL.matcher(body).find()) {
copyLink.setVisible(true);
}
}
@ -1611,6 +1633,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
switch (item.getItemId()) {
case R.id.encryption_choice_axolotl:
case R.id.encryption_choice_otr:
case R.id.encryption_choice_pgp:
case R.id.encryption_choice_none:
handleEncryptionSelection(item);
@ -1797,6 +1820,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
updated = conversation.setNextEncryption(Message.ENCRYPTION_NONE);
item.setChecked(true);
break;
case R.id.encryption_choice_otr:
updated = conversation.setNextEncryption(Message.ENCRYPTION_OTR);
item.setChecked(true);
break;
case R.id.encryption_choice_pgp:
if (activity.hasPgp()) {
if (conversation.getAccount().getPgpSignature() != null) {
@ -2011,6 +2038,20 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
}
private OnClickListener OTRwarning = new OnClickListener() {
@Override
public void onClick(View v) {
try {
final Uri uri = Uri.parse("https://monocles.wiki/index.php?title=Monocles_Chat");
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(browserIntent);
} catch (Exception e) {
ToastCompat.makeText(activity, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
}
}
};
@SuppressLint("InflateParams")
protected void clearHistoryDialog(final Conversation conversation) {
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
@ -2118,11 +2159,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
intent.putExtra("ALTERNATIVE_CODEC", activity.xmppConnectionService.alternativeVoiceSettings());
break;
case ATTACHMENT_CHOICE_LOCATION:
if (activity.xmppConnectionService.webViewAvailable()) {
intent = new Intent(getActivity(), ShareLocationActivity.class);
} else {
ToastCompat.makeText(activity, R.string.webview_not_available, ToastCompat.LENGTH_LONG).show();
}
intent = GeoHelper.getFetchIntent(activity);
break;
}
final Context context = getActivity();
@ -2662,11 +2699,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
});
builder.setPositiveButton(getString(R.string.ok),
(dialog, which) -> {
Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
try {
Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
intent.putExtra("uuid", conversation.getUuid());
startActivity(intent);
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
} catch (Exception e) {
e.printStackTrace();
}
});
builder.create().show();
});
@ -2808,6 +2849,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
private void updateSnackBar(final Conversation conversation) {
if (conversation == null) {
return;
}
final Account account = conversation.getAccount();
final XmppConnection connection = account.getXmppConnection();
final int mode = conversation.getMode();
@ -2882,9 +2926,24 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
} else if (account.hasPendingPgpIntent(conversation)) {
showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
} else if (activity.warnUnecryptedChat()) {
} else if (mode == Conversation.MODE_SINGLE
&& conversation.smpRequested()) {
showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
} else if (mode == Conversation.MODE_SINGLE
&& conversation.hasValidOtrSession()
&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
&& (!conversation.isOtrFingerprintVerified())) {
showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
} else if (connection != null
&& connection.getFeatures().blocking()
&& conversation.countMessages() != 0
&& !conversation.isBlocked()
&& conversation.isWithStranger()) {
showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener);
} else if (activity != null && activity.warnUnecryptedChat()) {
if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE && conversation.isSingleOrPrivateAndNonAnonymous() && ((Config.supportOmemo() && Conversation.suitableForOmemoByDefault(conversation)) ||
(Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()))) {
(Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()) || (
mode == Conversation.MODE_SINGLE && Config.supportOtr()))) {
if (ENCRYPTION_EXCEPTIONS.contains(conversation.getJid().toString()) || conversation.getJid().toString().equals(account.getJid().getDomain())) {
hideSnackbar();
} else {
@ -3275,7 +3334,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
builder.setPositiveButton(getString(R.string.send_unencrypted), listener);
builder.create().show();
}
protected void sendOtrMessage(final Message message) {
final ConversationsActivity activity = (ConversationsActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
activity.selectPresence(conversation,
() -> {
message.setCounterpart(conversation.getNextCounterpart());
xmppService.sendMessage(message);
messageSent();
});
}
public void appendText(String text, final boolean doNotAppend) {
if (text == null) {
return;

View file

@ -58,6 +58,10 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import net.java.otr4j.session.SessionStatus;
import androidx.appcompat.widget.PopupMenu;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
@ -206,13 +210,17 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
SharedPreferences.Editor editor = FirstStart.edit();
editor.putLong(PREF_FIRST_START, FirstStartTime);
editor.commit();
// restart
Intent restartintent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName());
restartintent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
restartintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(restartintent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
System.exit(0);
// restart if storage not accessable
if (FileBackend.getDiskSize() > 0) {
return;
} else {
Intent restartintent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName());
restartintent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
restartintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(restartintent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
System.exit(0);
}
}
if (useInternalUpdater()) {
@ -782,17 +790,17 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
pendingViewIntent.push(intent);
}
} else if (intent != null && ACTION_DESTROY_MUC.equals(intent.getAction())) {
final Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey("MUC_UUID")) {
Log.d(Config.LOGTAG, "Get " + intent.getAction() + " intent for " + extras.getString("MUC_UUID"));
Conversation conversation = xmppConnectionService.findConversationByUuid(extras.getString("MUC_UUID"));
try {
try {
final Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey("MUC_UUID")) {
Log.d(Config.LOGTAG, "Get " + intent.getAction() + " intent for " + extras.getString("MUC_UUID"));
Conversation conversation = xmppConnectionService.findConversationByUuid(extras.getString("MUC_UUID"));
ConversationsActivity.this.xmppConnectionService.clearConversationHistory(conversation);
xmppConnectionService.destroyRoom(conversation, ConversationsActivity.this);
endConversation(conversation);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
setIntent(createLauncherIntent(this));
@ -975,7 +983,32 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
}
}
public void verifyOtrSessionDialog(final Conversation conversation, View view) {
if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show();
return;
}
if (view == null) {
return;
}
PopupMenu popup = new PopupMenu(this, view);
popup.inflate(R.menu.verification_choices);
popup.setOnMenuItemClickListener(menuItem -> {
Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class);
intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
switch (menuItem.getItemId()) {
case R.id.ask_question:
intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
break;
}
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
return true;
});
popup.show();
}
@Override
public void onConversationArchived(Conversation conversation) {
if (performRedirectIfNecessary(conversation, false)) {

View file

@ -184,6 +184,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
jid = Jid.ofEscaped(binding.accountJid.getText().toString(), getUserModeDomain(), null);
} else {
jid = Jid.ofEscaped(binding.accountJid.getText().toString());
Resolver.checkDomain(jid);
}
} catch (final NullPointerException e) {
if (mUsernameMode) {

View file

@ -221,17 +221,7 @@ public class ImportBackupActivity extends XmppActivity implements ServiceConnect
@Override
public void onBackupRestored() {
runOnUiThread(this::restart);
}
private void restart() {
Log.d(Config.LOGTAG, "Restarting " + getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName()));
Intent intent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
System.exit(0);
}
@Override

View file

@ -87,6 +87,7 @@ public class SettingsActivity extends XmppActivity implements
public static final String MAPPREVIEW_HOST = "mappreview_host";
public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction";
public static final String ALLOW_MESSAGE_RETRACTION = "allow_message_retraction";
public static final String ENABLE_OTR_ENCRYPTION = "enable_otr_encryption";
public static final String USE_UNICOLORED_CHATBG = "unicolored_chatbg";
public static final String EASY_DOWNLOADER = "easy_downloader";
public static final String MIN_ANDROID_SDK21_SHOWN = "min_android_sdk21_shown";

View file

@ -2,6 +2,7 @@ package eu.siacs.conversations.ui;
import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER;
import androidx.annotation.RequiresApi;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@ -104,6 +105,13 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import me.drakeet.support.toast.ToastCompat;
import pl.droidsonroids.gif.GifDrawable;
import android.util.Pair;
import net.java.otr4j.session.SessionID;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.utils.CryptoHelper;
import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_OTR_ENCRYPTION;
public abstract class XmppActivity extends ActionBarActivity {
@ -418,7 +426,17 @@ public abstract class XmppActivity extends ActionBarActivity {
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
if (contact.showInRoster() || contact.isSelf()) {
if (conversation.hasValidOtrSession()) {
SessionID id = conversation.getOtrSession().getSessionID();
Jid jid;
try {
jid = Jid.of(id.getAccountID() + "/" + id.getUserID());
} catch (IllegalArgumentException e) {
jid = null;
}
conversation.setNextCounterpart(jid);
listener.onPresenceSelected();
} else if (contact.showInRoster() || contact.isSelf()) {
final Presences presences = contact.getPresences();
if (presences.size() == 0) {
if (contact.isSelf()) {
@ -484,7 +502,9 @@ public abstract class XmppActivity extends ActionBarActivity {
public boolean unicoloredBG() {
return getBooleanPreference("unicolored_chatbg", R.bool.use_unicolored_chatbg) || getPreferences().getString(SettingsActivity.THEME, getString(R.string.theme)).equals("black");
}
public boolean enableOTR() {
return getBooleanPreference(ENABLE_OTR_ENCRYPTION, R.bool.enable_otr);
}
public boolean showDateInQuotes() {
return getBooleanPreference("show_date_in_quotes", R.bool.show_date_in_quotes);
}
@ -522,12 +542,22 @@ public abstract class XmppActivity extends ActionBarActivity {
final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
return cm != null
&& cm.isActiveNetworkMetered()
&& cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
&& getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
} else {
return false;
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
private static int getRestrictBackgroundStatus(@NonNull final ConnectivityManager connectivityManager) {
try {
return connectivityManager.getRestrictBackgroundStatus();
} catch (final Exception e) {
Log.d(Config.LOGTAG, "platform bug detected. Unable to get restrict background status", e);
return -1;
}
}
protected boolean usingEnterKey() {
return getBooleanPreference("display_enter_key", R.bool.display_enter_key);
}

View file

@ -1276,10 +1276,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground);
} else {
/* todo why should we mark a file as deleted? --> causing strange side effects
if (!activity.xmppConnectionService.getFileBackend().getFile(message).exists() && !message.isFileDeleted()) {
markFileDeleted(message);
displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message);
}
}*/
if (checkFileExistence(message, view, viewHolder)) {
markFileExisting(message);
}
@ -1445,16 +1446,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
private void showLocation(Message message) {
if (activity.xmppConnectionService.webViewAvailable()) {
for (Intent intent : GeoHelper.createGeoIntentsFromMessage(this.getContext(), message)) {
if (intent.resolveActivity(getContext().getPackageManager()) != null) {
getContext().startActivity(intent);
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
return;
}
for (Intent intent : GeoHelper.createGeoIntentsFromMessage(this.getContext(), message)) {
if (intent.resolveActivity(getContext().getPackageManager()) != null) {
getContext().startActivity(intent);
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
return;
}
} else {
ToastCompat.makeText(activity, R.string.webview_not_available, ToastCompat.LENGTH_LONG).show();
}
}

View file

@ -104,6 +104,7 @@ public class ConversationMenuConfigurator {
return;
}
final MenuItem none = menu.findItem(R.id.encryption_choice_none);
final MenuItem otr = menu.findItem(R.id.encryption_choice_otr);
final MenuItem pgp = menu.findItem(R.id.encryption_choice_pgp);
final MenuItem axolotl = menu.findItem(R.id.encryption_choice_axolotl);
@ -131,11 +132,18 @@ public class ConversationMenuConfigurator {
if (conversation.getNextEncryption() != Message.ENCRYPTION_NONE) {
menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
}
otr.setVisible(Config.supportOtr() && activity.enableOTR());
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setVisible(false);
}
pgp.setVisible(Config.supportOpenPgp());
none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
axolotl.setVisible(Config.supportOmemo());
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_OTR:
menuSecure.setTitle(R.string.encryption_choice_otr);
otr.setChecked(true);
break;
case Message.ENCRYPTION_PGP:
menuSecure.setTitle(R.string.encrypted_with_openpgp);
pgp.setChecked(true);

View file

@ -287,7 +287,9 @@ public class MyLinkify {
if (end < cs.length()) {
// Reject strings that were probably matched only because they contain a dot followed by
// by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java)
return !isAlphabetic(cs.charAt(end - 1)) || !isAlphabetic(cs.charAt(end));
if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) {
return false;
}
}
return true;
};
@ -317,7 +319,7 @@ public class MyLinkify {
public static void addLinks(Editable body, boolean includeGeo) {
Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null);
Linkify.addLinks(body, Patterns.WEB_URL, "http", null, WEBURL_TRANSFORM_FILTER);
Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER);
if (includeGeo) {
Linkify.addLinks(body, GeoHelper.GEO_URI, "geo");
}

View file

@ -123,7 +123,7 @@ public class ShareUtil {
return;
}
}
Matcher webUrlPatternMatcher = Patterns.WEB_URL.matcher(body);
Matcher webUrlPatternMatcher = Patterns.AUTOLINK_WEB_URL.matcher(body);
if (webUrlPatternMatcher.find()) {
String url = body.substring(webUrlPatternMatcher.start(), webUrlPatternMatcher.end());
if (activity.copyTextToClipboard(url, R.string.web_address)) {

View file

@ -47,10 +47,6 @@ public class Compatibility {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
}
public static boolean runsTwentySix() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public static boolean runsTwentyThree() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
@ -59,6 +55,10 @@ public class Compatibility {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
public static boolean runsTwentySix() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public static boolean runsTwentyEight() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
}

View file

@ -131,7 +131,11 @@ public abstract class ConversationsFileObserver {
}
return;
}
ConversationsFileObserver.this.onEvent(event, file);
try {
ConversationsFileObserver.this.onEvent(event, file);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}

View file

@ -69,7 +69,7 @@ public class ExceptionHelper {
report.append("Device: ").append(deviceName).append('\n');
report.append("Android SDK: ").append(sdkVersion).append(" (").append(release).append(")").append('\n');
report.append("Version: ").append(packageInfo.versionName).append('\n');
report.append(String.format(Locale.ROOT, "Version: %s(%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)).append('\n');
report.append(String.format(Locale.ROOT, "Version: %s(%d) ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)).append('\n');
report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n');
Signature[] signatures = packageInfo.signatures;
if (signatures != null && signatures.length >= 1) {

View file

@ -25,6 +25,7 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.ShowLocationActivity;
import eu.siacs.conversations.ui.ShareLocationActivity;
public class GeoHelper {
@ -76,6 +77,9 @@ public class GeoHelper {
}
return false;
}
public static Intent getFetchIntent(Context context) {
return new Intent(context, ShareLocationActivity.class);
}
private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException {
Matcher matcher = GEO_URI.matcher(body);

View file

@ -46,6 +46,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
public class Resolver {
@ -92,6 +93,9 @@ public class Resolver {
}
return happyEyeball(resolveNoSrvRecords(DNSName.from(hostname), port, true));
}
public static void checkDomain(final Jid jid) {
DNSName.from(jid.getDomain());
}
public static boolean invalidHostname(final String hostname) {
try {

View file

@ -134,7 +134,7 @@ public class RichPreview {
}
private String resolveURL(String url, String part) {
if (Patterns.WEB_URL.matcher(part).matches() && !part.contains(" ")) {
if (Patterns.AUTOLINK_WEB_URL.matcher(part).matches() && !part.contains(" ")) {
return part;
} else {
URI base_uri = null;

View file

@ -511,9 +511,6 @@ public class UIHelper {
}
public static String getFileDescriptionString(final Context context, final Message message) {
if (message.getType() == Message.TYPE_IMAGE) {
return context.getString(R.string.image);
}
final String mime = message.getMimeType();
if (mime == null) {
return context.getString(R.string.file);

View file

@ -8,23 +8,18 @@ import java.io.OutputStreamWriter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
public class TagWriter {
private static final int FLUSH_DELAY = 400;
private OutputStreamWriter outputStream;
private boolean finished = false;
private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>();
private CountDownLatch stanzaWriterCountDownLatch = null;
private Thread asyncStanzaWriter = new Thread() {
private final AtomicInteger batchStanzaCount = new AtomicInteger(0);
@Override
public void run() {
stanzaWriterCountDownLatch = new CountDownLatch(1);
@ -33,21 +28,12 @@ public class TagWriter {
break;
}
try {
final AbstractStanza stanza = writeQueue.poll(FLUSH_DELAY, TimeUnit.MILLISECONDS);
if (stanza != null) {
batchStanzaCount.incrementAndGet();
outputStream.write(stanza.toString());
} else {
final int batch = batchStanzaCount.getAndSet(0);
if (batch > 1) {
Log.d(Config.LOGTAG, "flushing " + batch + " stanzas");
}
AbstractStanza output = writeQueue.take();
outputStream.write(output.toString());
if (writeQueue.size() == 0) {
outputStream.flush();
final AbstractStanza nextStanza = writeQueue.take();
batchStanzaCount.incrementAndGet();
outputStream.write(nextStanza.toString());
}
} catch (final Exception e) {
} catch (Exception e) {
break;
}
}

View file

@ -521,7 +521,7 @@ public class XmppConnection implements Runnable {
if (Namespace.SASL.equals(failure.getNamespace())) {
final String text = failure.findChildContent("text");
if (failure.hasChild("account-disabled") && text != null) {
Matcher matcher = Patterns.WEB_URL.matcher(text);
Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
if (matcher.find()) {
final HttpUrl url;
try {
@ -1050,7 +1050,7 @@ public class XmppConnection implements Runnable {
if (url != null) {
setAccountCreationFailed(url);
} else if (instructions != null) {
final Matcher matcher = Patterns.WEB_URL.matcher(instructions);
final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
if (matcher.find()) {
setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end()));
}

View file

@ -1199,4 +1199,5 @@
<string name="verifying_omemo_keys_trusted_source_account">Estás a punto de verificar las claves OMEMO de tu propia cuenta. Esto es únicamente seguro si has seguido este enlace desde un origen de confianza donde solo tú puedes haber este enlace.</string>
<string name="continue_btn">Continuar</string>
<string name="participants">Participantes</string>
<string name="download_failed_invalid_file">Descarga fallida: Fichero no válido</string>
</resources>

View file

@ -1201,4 +1201,5 @@
<string name="verifying_omemo_keys_trusted_source_account">You are about to verify the OMEMO keys of your own account. This is only secure if you followed this link from a trusted source where only you could have published this link.</string>
<string name="continue_btn">Continue</string>
<string name="participants">Participants</string>
<string name="download_failed_invalid_file">Download failed: Invalid file</string>
</resources>

View file

@ -509,6 +509,11 @@
android:key="delete_omemo_identities"
android:summary="@string/pref_delete_omemo_identities_summary"
android:title="@string/pref_delete_omemo_identities" />
<CheckBoxPreference
android:defaultValue="@bool/enable_otr"
android:key="enable_otr_encryption"
android:summary="@string/pref_enable_otr_summary"
android:title="@string/pref_enable_otr" />
<CheckBoxPreference
android:defaultValue="@bool/dont_trust_system_cas"
android:key="dont_trust_system_cas"