aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java16
-rw-r--r--src/main/java/eu/siacs/conversations/OmemoActivity.java290
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java6
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java9
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java250
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java180
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java45
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java6
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java167
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java50
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Contact.java10
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java38
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java55
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java42
-rw-r--r--src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java6
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java16
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java11
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java17
-rw-r--r--src/main/java/eu/siacs/conversations/parser/AbstractParser.java31
-rw-r--r--src/main/java/eu/siacs/conversations/parser/IqParser.java9
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java34
-rw-r--r--src/main/java/eu/siacs/conversations/parser/PresenceParser.java16
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java207
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java38
-rw-r--r--src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java2
-rw-r--r--src/main/java/eu/siacs/conversations/services/AvatarService.java5
-rw-r--r--src/main/java/eu/siacs/conversations/services/BarcodeProvider.java206
-rw-r--r--src/main/java/eu/siacs/conversations/services/EventReceiver.java3
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java3
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java360
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java75
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java34
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java59
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java172
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditMessage.java36
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsActivity.java23
-rw-r--r--src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java45
-rw-r--r--src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java116
-rw-r--r--src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java5
-rw-r--r--src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java29
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java223
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java133
-rw-r--r--src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java66
-rw-r--r--src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java201
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java6
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java41
-rw-r--r--src/main/java/eu/siacs/conversations/utils/FileWriterException.java4
-rw-r--r--src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java69
-rw-r--r--src/main/java/eu/siacs/conversations/utils/Xmlns.java1
-rw-r--r--src/main/java/eu/siacs/conversations/utils/XmppUri.java104
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java165
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java31
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java16
54 files changed, 2675 insertions, 1109 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 28496b37..ee3db65b 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -54,17 +54,13 @@ public final class Config {
public static final int PING_MAX_INTERVAL = 300;
public static final int IDLE_PING_INTERVAL = 600; //540 is minimum according to docs;
public static final int PING_MIN_INTERVAL = 30;
+ public static final int LOW_PING_TIMEOUT = 1; // used after push received
public static final int PING_TIMEOUT = 15;
public static final int SOCKET_TIMEOUT = 15;
public static final int CONNECT_TIMEOUT = 90;
public static final int CONNECT_DISCO_TIMEOUT = 20;
public static final int MINI_GRACE_PERIOD = 750;
- public static final boolean PUSH_MODE = false; //closes the tcp connection when going to background
- //and after PING_MIN_INTERVAL of inactivity
- //very experimental. only enable this if you want
- //to around with GCM push
-
public static final int AVATAR_SIZE = 192;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
@@ -82,6 +78,10 @@ public final class Config {
public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096;
+ public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+
+ public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY;
+
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance
@@ -97,16 +97,18 @@ public final class Config {
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
- public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
+ public static final boolean ONLY_INTERNAL_STORAGE = true; //use internal storage instead of sdcard to save attachments
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
public static final boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesn’t filter
- public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
public static final int MAM_MAX_MESSAGES = 500;
+ public static final long FREQUENT_RESTARTS_DETECTION_WINDOW = 12 * 60 * 60 * 1000; // 10 hours
+ public static final long FREQUENT_RESTARTS_THRESHOLD = 16;
+
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
public static final int TYPING_TIMEOUT = 8;
diff --git a/src/main/java/eu/siacs/conversations/OmemoActivity.java b/src/main/java/eu/siacs/conversations/OmemoActivity.java
new file mode 100644
index 00000000..c0c7b298
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/OmemoActivity.java
@@ -0,0 +1,290 @@
+package eu.siacs.conversations;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.TrustKeysActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.widget.Switch;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.XmppUri;
+
+
+public abstract class OmemoActivity extends XmppActivity {
+
+ private Account mSelectedAccount;
+ private String mSelectedFingerprint;
+
+ protected XmppUri mPendingFingerprintVerificationUri = null;
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu,v,menuInfo);
+ Object account = v.getTag(R.id.TAG_ACCOUNT);
+ Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT);
+ Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS);;
+ if (account != null
+ && fingerprint != null
+ && account instanceof Account
+ && fingerprintStatus != null
+ && fingerprint instanceof String
+ && fingerprintStatus instanceof FingerprintStatus) {
+ getMenuInflater().inflate(R.menu.omemo_key_context, menu);
+ MenuItem purgeItem = menu.findItem(R.id.purge_omemo_key);
+ MenuItem verifyScan = menu.findItem(R.id.verify_scan);
+ if (this instanceof TrustKeysActivity) {
+ purgeItem.setVisible(false);
+ verifyScan.setVisible(false);
+ } else {
+ FingerprintStatus status = (FingerprintStatus) fingerprintStatus;
+ if (!status.isActive() || status.isVerified()) {
+ verifyScan.setVisible(false);
+ }
+ }
+ this.mSelectedAccount = (Account) account;
+ this.mSelectedFingerprint = (String) fingerprint;
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.purge_omemo_key:
+ showPurgeKeyDialog(mSelectedAccount,mSelectedFingerprint);
+ break;
+ case R.id.copy_omemo_key:
+ copyOmemoFingerprint(mSelectedFingerprint);
+ break;
+ case R.id.verify_scan:
+ new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+ if (scanResult != null && scanResult.getFormatName() != null) {
+ String data = scanResult.getContents();
+ XmppUri uri = new XmppUri(data);
+ if (xmppConnectionServiceBound) {
+ processFingerprintVerification(uri);
+ } else {
+ this.mPendingFingerprintVerificationUri =uri;
+ }
+ }
+ }
+
+ protected abstract void processFingerprintVerification(XmppUri uri);
+
+ protected void copyOmemoFingerprint(String fingerprint) {
+ if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), R.string.omemo_fingerprint)) {
+ Toast.makeText(
+ this,
+ R.string.toast_message_omemo_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ protected void addFingerprintRow(LinearLayout keys, final XmppAxolotlSession session, boolean highlight) {
+ final Account account = session.getAccount();
+ final String fingerprint = session.getFingerprint();
+ addFingerprintRowWithListeners(keys,
+ session.getAccount(),
+ session.getFingerprint(),
+ highlight,
+ session.getTrust(),
+ true,
+ true,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(isChecked));
+ }
+ });
+ }
+
+ protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account,
+ final String fingerprint,
+ boolean highlight,
+ FingerprintStatus status,
+ boolean showTag,
+ boolean undecidedNeedEnablement,
+ CompoundButton.OnCheckedChangeListener
+ onCheckedChangeListener) {
+ View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView keyType = (TextView) view.findViewById(R.id.key_type);
+ if (Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
+ View.OnClickListener listener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showX509Certificate(account,fingerprint);
+ }
+ };
+ key.setOnClickListener(listener);
+ keyType.setOnClickListener(listener);
+ }
+ Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
+ ImageView verifiedFingerprintSymbol = (ImageView) view.findViewById(R.id.verified_fingerprint);
+ trustToggle.setVisibility(View.VISIBLE);
+ registerForContextMenu(view);
+ view.setTag(R.id.TAG_ACCOUNT,account);
+ view.setTag(R.id.TAG_FINGERPRINT,fingerprint);
+ view.setTag(R.id.TAG_FINGERPRINT_STATUS,status);
+ boolean x509 = Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509;
+ final View.OnClickListener toast;
+ trustToggle.setChecked(status.isTrusted(), false);
+
+ if (status.isActive()){
+ key.setTextColor(getPrimaryTextColor());
+ keyType.setTextColor(getSecondaryTextColor());
+ if (status.isVerified()) {
+ verifiedFingerprintSymbol.setVisibility(View.VISIBLE);
+ verifiedFingerprintSymbol.setAlpha(1.0f);
+ trustToggle.setVisibility(View.GONE);
+ verifiedFingerprintSymbol.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ replaceToast(getString(R.string.this_device_has_been_verified), false);
+ }
+ });
+ toast = null;
+ } else {
+ verifiedFingerprintSymbol.setVisibility(View.GONE);
+ trustToggle.setVisibility(View.VISIBLE);
+ trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
+ if (status.getTrust() == FingerprintStatus.Trust.UNDECIDED && undecidedNeedEnablement) {
+ trustToggle.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ account.getAxolotlService().setFingerprintTrust(fingerprint,FingerprintStatus.createActive(false));
+ v.setEnabled(true);
+ v.setOnClickListener(null);
+ }
+ });
+ trustToggle.setEnabled(false);
+ } else {
+ trustToggle.setOnClickListener(null);
+ trustToggle.setEnabled(true);
+ }
+ toast = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ hideToast();
+ }
+ };
+ }
+ } else {
+ key.setTextColor(getTertiaryTextColor());
+ keyType.setTextColor(getTertiaryTextColor());
+ toast = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
+ }
+ };
+ if (status.isVerified()) {
+ trustToggle.setVisibility(View.GONE);
+ verifiedFingerprintSymbol.setVisibility(View.VISIBLE);
+ verifiedFingerprintSymbol.setAlpha(0.4368f);
+ verifiedFingerprintSymbol.setOnClickListener(toast);
+ } else {
+ trustToggle.setVisibility(View.VISIBLE);
+ verifiedFingerprintSymbol.setVisibility(View.GONE);
+ trustToggle.setOnClickListener(null);
+ trustToggle.setEnabled(false);
+ trustToggle.setOnClickListener(toast);
+ }
+ }
+
+ view.setOnClickListener(toast);
+ key.setOnClickListener(toast);
+ keyType.setOnClickListener(toast);
+ if (showTag) {
+ keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
+ } else {
+ keyType.setVisibility(View.GONE);
+ }
+ if (highlight) {
+ keyType.setTextColor(getResources().getColor(R.color.accent));
+ keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
+ } else {
+ keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
+ }
+
+ key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
+
+ keys.addView(view);
+ }
+
+ public void showPurgeKeyDialog(final Account account, final String fingerprint) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.purge_key));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.purge_key_desc_part1)
+ + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
+ + "\n\n" + getString(R.string.purge_key_desc_part2));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.purge_key),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ account.getAxolotlService().purgeKey(fingerprint);
+ refreshUi();
+ }
+ });
+ builder.create().show();
+ }
+
+ private void showX509Certificate(Account account, String fingerprint) {
+ X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint);
+ if (x509Certificate != null) {
+ showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate));
+ } else {
+ Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void showCertificateInformationDialog(Bundle bundle) {
+ View view = getLayoutInflater().inflate(R.layout.certificate_information, null);
+ final String not_available = getString(R.string.certicate_info_not_available);
+ TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn);
+ TextView subject_o = (TextView) view.findViewById(R.id.subject_o);
+ TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn);
+ TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o);
+ TextView sha1 = (TextView) view.findViewById(R.id.sha1);
+
+ subject_cn.setText(bundle.getString("subject_cn", not_available));
+ subject_o.setText(bundle.getString("subject_o", not_available));
+ issuer_cn.setText(bundle.getString("issuer_cn", not_available));
+ issuer_o.setText(bundle.getString("issuer_o", not_available));
+ sha1.setText(bundle.getString("sha1", not_available));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.certificate_information);
+ builder.setView(view);
+ builder.setPositiveButton(R.string.ok, null);
+ builder.create().show();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java
index 63f846c5..87b58613 100644
--- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java
@@ -134,9 +134,10 @@ public class PgpDecryptionService {
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
synchronized (PgpDecryptionService.this) {
+ PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
messages.addFirst(message);
currentMessage = null;
- storePendingIntent((PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT));
+ storePendingIntent(pendingIntent);
}
break;
case OpenPgpApi.RESULT_CODE_ERROR:
@@ -164,9 +165,10 @@ public class PgpDecryptionService {
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
synchronized (PgpDecryptionService.this) {
+ PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
messages.addFirst(message);
currentMessage = null;
- storePendingIntent((PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT));
+ storePendingIntent(pendingIntent);
}
break;
case OpenPgpApi.RESULT_CODE_ERROR:
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
index da315812..1a5367fd 100644
--- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -259,8 +259,13 @@ public class PgpEngine {
account);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
- logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
- callback.error(R.string.unable_to_connect_to_keychain, account);
+ OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+ if (error != null && "signing subkey not found!".equals(error.getMessage())) {
+ callback.error(0,account);
+ } else {
+ logError(account, error);
+ callback.error(R.string.unable_to_connect_to_keychain, null);
+ }
}
}
});
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
index 1bc5fa83..09beb22c 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -25,13 +25,17 @@ import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
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.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
@@ -73,6 +77,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private int numPublishTriesOnEmptyPep = 0;
private boolean pepBroken = false;
+ private AtomicBoolean ownPushPending = new AtomicBoolean(false);
+
@Override
public void onAdvancedStreamFeaturesAvailable(Account account) {
if (Config.supportOmemo()
@@ -88,7 +94,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
for(Jid jid : jids) {
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
- AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+ AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
return true;
}
@@ -98,6 +104,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return false;
}
+ public void preVerifyFingerprint(Contact contact, String fingerprint) {
+ axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint);
+ }
+
+ public void preVerifyFingerprint(Account account, String fingerprint) {
+ axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint);
+ }
+
+ public boolean hasVerifiedKeys(String name) {
+ for(XmppAxolotlSession session : this.sessions.getAll(new AxolotlAddress(name,0)).values()) {
+ if (session.getTrust().isVerified()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static class AxolotlAddressMap<T> {
protected Map<String, Map<Integer, T>> map;
protected final Object MAP_LOCK = new Object();
@@ -164,7 +187,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
for (Integer deviceId : deviceIds) {
AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
if(Config.X509_VERIFICATION) {
X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", ""));
@@ -185,8 +207,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private void fillMap(SQLiteAxolotlStore store) {
- List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
- putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
+ List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString());
+ putDevicesForJid(account.getJid().toBareJid().toPreppedString(), deviceIds, store);
for (Contact contact : account.getRoster().getContacts()) {
Jid bareJid = contact.getJid().toBareJid();
String address = bareJid.toString();
@@ -200,7 +222,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public void put(AxolotlAddress address, XmppAxolotlSession value) {
super.put(address, value);
value.setNotFresh();
- xmppConnectionService.syncRosterToDisk(account);
+ xmppConnectionService.syncRosterToDisk(account); //TODO why?
}
public void put(XmppAxolotlSession session) {
@@ -213,6 +235,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
SUCCESS,
SUCCESS_VERIFIED,
TIMEOUT,
+ SUCCESS_TRUSTED,
ERROR
}
@@ -220,7 +243,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public void clearErrorFor(Jid jid) {
synchronized (MAP_LOCK) {
- Map<Integer, FetchStatus> devices = this.map.get(jid.toBareJid().toString());
+ Map<Integer, FetchStatus> devices = this.map.get(jid.toBareJid().toPreppedString());
if (devices == null) {
return;
}
@@ -256,29 +279,29 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", "");
}
- public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
- return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
+ public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
+ return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status);
}
- public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
- return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust);
+ public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
+ return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status);
}
- public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
+ public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) {
Set<IdentityKey> keys = new HashSet<>();
for(Jid jid : jids) {
- keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust));
+ keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status));
}
return keys;
}
public long getNumTrustedKeys(Jid jid) {
- return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString());
+ return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString());
}
public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
for(Jid jid : jids) {
- if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) {
+ if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) {
return true;
}
}
@@ -286,17 +309,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private AxolotlAddress getAddressForJid(Jid jid) {
- return new AxolotlAddress(jid.toString(), 0);
+ return new AxolotlAddress(jid.toPreppedString(), 0);
}
- private Set<XmppAxolotlSession> findOwnSessions() {
+ public Collection<XmppAxolotlSession> findOwnSessions() {
AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
- return new HashSet<>(this.sessions.getAll(ownAddress).values());
+ ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress).values());
+ Collections.sort(s);
+ return s;
}
- private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) {
+
+
+ public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
- return new HashSet<>(this.sessions.getAll(contactAddress).values());
+ ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress).values());
+ Collections.sort(s);
+ return s;
}
private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
@@ -307,22 +336,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return sessions;
}
- public Set<String> getFingerprintsForOwnSessions() {
- Set<String> fingerprints = new HashSet<>();
- for (XmppAxolotlSession session : findOwnSessions()) {
- fingerprints.add(session.getFingerprint());
- }
- return fingerprints;
- }
-
- public Set<String> getFingerprintsForContact(final Contact contact) {
- Set<String> fingerprints = new HashSet<>();
- for (XmppAxolotlSession session : findSessionsForContact(contact)) {
- fingerprints.add(session.getFingerprint());
- }
- return fingerprints;
- }
-
private boolean hasAny(Jid jid) {
return sessions.hasAny(getAddressForJid(jid));
}
@@ -355,58 +368,54 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return this.deviceIds.get(account.getJid().toBareJid());
}
- private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
- final XmppAxolotlSession.Trust from,
- final XmppAxolotlSession.Trust to) {
- for (Integer deviceId : deviceIds) {
- AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
+ public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
+ boolean me = jid.toBareJid().equals(account.getJid().toBareJid());
+ if (me && ownPushPending.getAndSet(false)) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring own device update because of pending push");
+ return;
+ }
+ boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId());
+ if (me) {
+ deviceIds.remove(getOwnDeviceId());
+ }
+ Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
+ expiredDevices.removeAll(deviceIds);
+ for (Integer deviceId : expiredDevices) {
+ AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
- if (session != null && session.getFingerprint() != null
- && session.getTrust() == from) {
- session.setTrust(to);
+ if (session != null && session.getFingerprint() != null) {
+ if (session.getTrust().isActive()) {
+ session.setTrust(session.getTrust().toInactive());
+ }
}
}
- }
-
- public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
- if (jid.toBareJid().equals(account.getJid().toBareJid())) {
- if (!deviceIds.isEmpty()) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attempts and pepBroken status.");
- pepBroken = false;
- numPublishTriesOnEmptyPep = 0;
+ Set<Integer> newDevices = new HashSet<>(deviceIds);
+ for (Integer deviceId : newDevices) {
+ AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
+ XmppAxolotlSession session = sessions.get(address);
+ if (session != null && session.getFingerprint() != null) {
+ if (!session.getTrust().isActive()) {
+ Log.d(Config.LOGTAG,"reactivating device with fingprint "+session.getFingerprint());
+ session.setTrust(session.getTrust().toActive());
+ }
}
- if (deviceIds.contains(getOwnDeviceId())) {
- deviceIds.remove(getOwnDeviceId());
- } else {
- publishOwnDeviceId(deviceIds);
+ }
+ if (me) {
+ if (Config.OMEMO_AUTO_EXPIRY != 0) {
+ needsPublishing |= deviceIds.removeAll(getExpiredDevices());
}
for (Integer deviceId : deviceIds) {
- AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
+ AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) {
buildSessionFromPEP(ownDeviceAddress);
}
}
+ if (needsPublishing) {
+ publishOwnDeviceId(deviceIds);
+ }
}
- Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
- expiredDevices.removeAll(deviceIds);
- setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
- XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
- setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509,
- XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
- setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
- XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
- setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
- XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
- Set<Integer> newDevices = new HashSet<>(deviceIds);
- setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
- XmppAxolotlSession.Trust.TRUSTED);
- setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509,
- XmppAxolotlSession.Trust.TRUSTED_X509);
- setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
- XmppAxolotlSession.Trust.UNDECIDED);
- setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
- XmppAxolotlSession.Trust.UNTRUSTED);
this.deviceIds.put(jid, deviceIds);
+ mXmppConnectionService.updateConversationUi(); //update the lock icon
mXmppConnectionService.keyStatusUpdated(null);
}
@@ -419,16 +428,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
deviceIds.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
- mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
- @Override
- public void onIqPacketReceived(Account account, IqPacket packet) {
- // TODO: implement this!
- }
- });
+ mXmppConnectionService.sendIqPacket(account, publish, null);
}
public void purgeKey(final String fingerprint) {
- axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
+ axolotlStore.setFingerprintStatus(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised());
}
public void publishOwnDeviceIdIfNeeded() {
@@ -445,14 +449,30 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} else {
Element item = mXmppConnectionService.getIqParser().getItem(packet);
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
- if (!deviceIds.contains(getOwnDeviceId())) {
- publishOwnDeviceId(deviceIds);
- }
+ registerDevices(account.getJid().toBareJid(),deviceIds);
}
}
});
}
+ private Set<Integer> getExpiredDevices() {
+ Set<Integer> devices = new HashSet<>();
+ for(XmppAxolotlSession session : findOwnSessions()) {
+ if (session.getTrust().isActive()) {
+ long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
+ if (diff > Config.OMEMO_AUTO_EXPIRY) {
+ long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint());
+ if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
+ devices.add(session.getRemoteAddress().getDeviceId());
+ session.setTrust(session.getTrust().toInactive());
+ Log.d(Config.LOGTAG, "added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+(lastMessageDiff/1000)+"s ago");
+ }
+ }
+ }
+ }
+ return devices;
+ }
+
public void publishOwnDeviceId(Set<Integer> deviceIds) {
Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
if (!deviceIdsCopy.contains(getOwnDeviceId())) {
@@ -471,9 +491,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
deviceIdsCopy.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
+ ownPushPending.set(true);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
+ ownPushPending.set(false);
if (packet.getType() == IqPacket.TYPE.ERROR) {
pepBroken = true;
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
@@ -692,16 +714,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return jids;
}
- public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
- return axolotlStore.getFingerprintTrust(fingerprint);
+ public FingerprintStatus getFingerprintTrust(String fingerprint) {
+ return axolotlStore.getFingerprintStatus(fingerprint);
}
public X509Certificate getFingerprintCertificate(String fingerprint) {
return axolotlStore.getFingerprintCertificate(fingerprint);
}
- public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
- axolotlStore.setFingerprintTrust(fingerprint, trust);
+ public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
+ axolotlStore.setFingerprintStatus(fingerprint, status);
}
private void verifySessionWithPEP(final XmppAxolotlSession session) {
@@ -724,7 +746,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
String fingerprint = session.getFingerprint();
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
- setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
+ setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
@@ -759,15 +781,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
- AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
- if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
- && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
+ Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress);
+ Map<Integer, FetchStatus> remote = fetchStatusMap.getAll(address);
+ if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) {
FetchStatus report = null;
- if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
- | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
+ if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) {
+ report = FetchStatus.SUCCESS;
+ } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) {
report = FetchStatus.SUCCESS_VERIFIED;
- } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
- || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
+ } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) {
+ report = FetchStatus.SUCCESS_TRUSTED;
+ } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) {
report = FetchStatus.ERROR;
}
mXmppConnectionService.keyStatusUpdated(report);
@@ -823,7 +848,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (Config.X509_VERIFICATION) {
verifySessionWithPEP(session);
} else {
- fetchStatusMap.put(address, FetchStatus.SUCCESS);
+ FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s",""));
+ FetchStatus fetchStatus;
+ if (status != null && status.isVerified()) {
+ fetchStatus = FetchStatus.SUCCESS_VERIFIED;
+ } else if (status != null && status.isTrusted()) {
+ fetchStatus = FetchStatus.SUCCESS_TRUSTED;
+ } else {
+ fetchStatus = FetchStatus.SUCCESS;
+ }
+ fetchStatusMap.put(address, fetchStatus);
finishBuildingSessionsFromPEP(address);
}
} catch (UntrustedIdentityException | InvalidKeyException e) {
@@ -850,7 +884,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
- AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+ AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
@@ -873,7 +907,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
if (deviceIds.get(account.getJid().toBareJid()) != null) {
for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
- AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
+ AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
@@ -921,8 +955,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
sessions.addAll(findOwnSessions());
boolean verified = false;
for(XmppAxolotlSession session : sessions) {
- if (session.getTrust().trusted()) {
- if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) {
+ if (session.getTrust().isTrustedAndActive()) {
+ if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
verified = true;
} else {
return false;
@@ -933,12 +967,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
- AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
return true;
}
for(Jid jid : jids) {
- AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0);
+ AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), 0);
if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
return true;
}
@@ -952,18 +986,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
account.getJid().toBareJid(), getOwnDeviceId());
Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
- Set<XmppAxolotlSession> ownSessions = findOwnSessions();
+ Collection<XmppAxolotlSession> ownSessions = findOwnSessions();
if (remoteSessions.isEmpty()) {
return null;
}
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
for (XmppAxolotlSession session : remoteSessions) {
- Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
for (XmppAxolotlSession session : ownSessions) {
- Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
@@ -1038,7 +1068,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
- AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
+ AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toPreppedString(),
message.getSenderDeviceId());
XmppAxolotlSession session = sessions.get(senderAddress);
if (session == null) {
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java
new file mode 100644
index 00000000..31b2264b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java
@@ -0,0 +1,180 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class FingerprintStatus implements Comparable<FingerprintStatus> {
+
+ private static final long DO_NOT_OVERWRITE = -1;
+
+ private Trust trust = Trust.UNTRUSTED;
+ private boolean active = false;
+ private long lastActivation = DO_NOT_OVERWRITE;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FingerprintStatus that = (FingerprintStatus) o;
+
+ return active == that.active && trust == that.trust;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = trust.hashCode();
+ result = 31 * result + (active ? 1 : 0);
+ return result;
+ }
+
+ private FingerprintStatus() {
+
+
+ }
+
+ public ContentValues toContentValues() {
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put(SQLiteAxolotlStore.TRUST,trust.toString());
+ contentValues.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
+ if (lastActivation != DO_NOT_OVERWRITE) {
+ contentValues.put(SQLiteAxolotlStore.LAST_ACTIVATION,lastActivation);
+ }
+ return contentValues;
+ }
+
+ public static FingerprintStatus fromCursor(Cursor cursor) {
+ final FingerprintStatus status = new FingerprintStatus();
+ try {
+ status.trust = Trust.valueOf(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.TRUST)));
+ } catch(IllegalArgumentException e) {
+ status.trust = Trust.UNTRUSTED;
+ }
+ status.active = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.ACTIVE)) > 0;
+ status.lastActivation = cursor.getLong(cursor.getColumnIndex(SQLiteAxolotlStore.LAST_ACTIVATION));
+ return status;
+ }
+
+ public static FingerprintStatus createActiveUndecided() {
+ final FingerprintStatus status = new FingerprintStatus();
+ status.trust = Trust.UNDECIDED;
+ status.active = true;
+ status.lastActivation = System.currentTimeMillis();
+ return status;
+ }
+
+ public static FingerprintStatus createActiveTrusted() {
+ final FingerprintStatus status = new FingerprintStatus();
+ status.trust = Trust.TRUSTED;
+ status.active = true;
+ status.lastActivation = System.currentTimeMillis();
+ return status;
+ }
+
+ public static FingerprintStatus createActiveVerified(boolean x509) {
+ final FingerprintStatus status = new FingerprintStatus();
+ status.trust = x509 ? Trust.VERIFIED_X509 : Trust.VERIFIED;
+ status.active = true;
+ return status;
+ }
+
+ public static FingerprintStatus createActive(boolean trusted) {
+ final FingerprintStatus status = new FingerprintStatus();
+ status.trust = trusted ? Trust.TRUSTED : Trust.UNTRUSTED;
+ status.active = true;
+ return status;
+ }
+
+ public boolean isTrustedAndActive() {
+ return active && isTrusted();
+ }
+
+ public boolean isTrusted() {
+ return trust == Trust.TRUSTED || isVerified();
+ }
+
+ public boolean isVerified() {
+ return trust == Trust.VERIFIED || trust == Trust.VERIFIED_X509;
+ }
+
+ public boolean isCompromised() {
+ return trust == Trust.COMPROMISED;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public FingerprintStatus toActive() {
+ FingerprintStatus status = new FingerprintStatus();
+ status.trust = trust;
+ if (!status.active) {
+ status.lastActivation = System.currentTimeMillis();
+ }
+ status.active = true;
+ return status;
+ }
+
+ public FingerprintStatus toInactive() {
+ FingerprintStatus status = new FingerprintStatus();
+ status.trust = trust;
+ status.active = false;
+ return status;
+ }
+
+ public Trust getTrust() {
+ return trust;
+ }
+
+ public static FingerprintStatus createCompromised() {
+ FingerprintStatus status = new FingerprintStatus();
+ status.active = false;
+ status.trust = Trust.COMPROMISED;
+ return status;
+ }
+
+ public FingerprintStatus toVerified() {
+ FingerprintStatus status = new FingerprintStatus();
+ status.active = active;
+ status.trust = Trust.VERIFIED;
+ return status;
+ }
+
+ public static FingerprintStatus createInactiveVerified() {
+ final FingerprintStatus status = new FingerprintStatus();
+ status.trust = Trust.VERIFIED;
+ status.active = false;
+ return status;
+ }
+
+ @Override
+ public int compareTo(FingerprintStatus o) {
+ if (active == o.active) {
+ if (lastActivation > o.lastActivation) {
+ return -1;
+ } else if (lastActivation < o.lastActivation) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (active){
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+ public long getLastActivation() {
+ return lastActivation;
+ }
+
+ public enum Trust {
+ COMPROMISED,
+ UNDECIDED,
+ UNTRUSTED,
+ TRUSTED,
+ VERIFIED,
+ VERIFIED_X509
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
index 4eb73313..13858b74 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
@@ -21,7 +21,10 @@ import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
public class SQLiteAxolotlStore implements AxolotlStore {
@@ -35,7 +38,10 @@ public class SQLiteAxolotlStore implements AxolotlStore {
public static final String KEY = "key";
public static final String FINGERPRINT = "fingerprint";
public static final String NAME = "name";
- public static final String TRUSTED = "trusted";
+ public static final String TRUSTED = "trusted"; //no longer used
+ public static final String TRUST = "trust";
+ public static final String ACTIVE = "active";
+ public static final String LAST_ACTIVATION = "last_activation";
public static final String OWN = "ownkey";
public static final String CERTIFICATE = "certificate";
@@ -51,11 +57,11 @@ public class SQLiteAxolotlStore implements AxolotlStore {
private int localRegistrationId;
private int currentPreKeyId = 0;
- private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
- new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
+ private final LruCache<String, FingerprintStatus> trustCache =
+ new LruCache<String, FingerprintStatus>(NUM_TRUSTS_TO_CACHE) {
@Override
- protected XmppAxolotlSession.Trust create(String fingerprint) {
- return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
+ protected FingerprintStatus create(String fingerprint) {
+ return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint);
}
};
@@ -185,7 +191,20 @@ public class SQLiteAxolotlStore implements AxolotlStore {
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
- mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
+ String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
+ FingerprintStatus status = getFingerprintStatus(fingerprint);
+ if (status == null) {
+ if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(name)) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": blindly trusted "+fingerprint+" of "+name);
+ status = FingerprintStatus.createActiveTrusted();
+ } else {
+ status = FingerprintStatus.createActiveUndecided();
+ }
+ } else {
+ status = status.toActive();
+ }
+ mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status);
+ trustCache.remove(fingerprint);
}
}
@@ -208,12 +227,12 @@ public class SQLiteAxolotlStore implements AxolotlStore {
return true;
}
- public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
+ public FingerprintStatus getFingerprintStatus(String fingerprint) {
return (fingerprint == null)? null : trustCache.get(fingerprint);
}
- public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
- mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
+ public void setFingerprintStatus(String fingerprint, FingerprintStatus status) {
+ mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status);
trustCache.remove(fingerprint);
}
@@ -225,8 +244,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint);
}
- public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
- return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
+ public Set<IdentityKey> getContactKeysWithTrust(String bareJid, FingerprintStatus status) {
+ return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status);
}
public long getContactNumTrustedKeys(String bareJid) {
@@ -428,4 +447,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
public void removeSignedPreKey(int signedPreKeyId) {
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
}
+
+ public void preVerifyFingerprint(Account account, String name, String fingerprint) {
+ mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified());
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
index 0b3164f8..981b93ec 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
@@ -204,13 +204,13 @@ public class XmppAxolotlMessage {
for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
Element keyElement = new Element(KEYTAG);
keyElement.setAttribute(REMOTEID, keyEntry.getKey());
- keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
+ keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.NO_WRAP));
headerElement.addChild(keyElement);
}
- headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
+ headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP));
if (ciphertext != null) {
Element payload = encryptionElement.addChild(PAYLOAD);
- payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
+ payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP));
}
return encryptionElement;
}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
index b7d11ec0..725757a3 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
@@ -19,13 +19,10 @@ import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
-import java.util.HashMap;
-import java.util.Map;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
-public class XmppAxolotlSession {
+public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> {
private final SessionCipher cipher;
private final SQLiteAxolotlStore sqLiteAxolotlStore;
private final AxolotlAddress remoteAddress;
@@ -34,76 +31,6 @@ public class XmppAxolotlSession {
private Integer preKeyId = null;
private boolean fresh = true;
- public enum Trust {
- UNDECIDED(0),
- TRUSTED(1),
- UNTRUSTED(2),
- COMPROMISED(3),
- INACTIVE_TRUSTED(4),
- INACTIVE_UNDECIDED(5),
- INACTIVE_UNTRUSTED(6),
- TRUSTED_X509(7),
- INACTIVE_TRUSTED_X509(8);
-
- private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
-
- static {
- for (Trust trust : Trust.values()) {
- trustsByValue.put(trust.getCode(), trust);
- }
- }
-
- private final int code;
-
- Trust(int code) {
- this.code = code;
- }
-
- public int getCode() {
- return this.code;
- }
-
- public String toString() {
- switch (this) {
- case UNDECIDED:
- return "Trust undecided " + getCode();
- case TRUSTED:
- return "Trusted " + getCode();
- case COMPROMISED:
- return "Compromised " + getCode();
- case INACTIVE_TRUSTED:
- return "Inactive (Trusted)" + getCode();
- case INACTIVE_UNDECIDED:
- return "Inactive (Undecided)" + getCode();
- case INACTIVE_UNTRUSTED:
- return "Inactive (Untrusted)" + getCode();
- case TRUSTED_X509:
- return "Trusted (X509) " + getCode();
- case INACTIVE_TRUSTED_X509:
- return "Inactive (Trusted (X509)) " + getCode();
- case UNTRUSTED:
- default:
- return "Untrusted " + getCode();
- }
- }
-
- public static Trust fromBoolean(Boolean trusted) {
- return trusted ? TRUSTED : UNTRUSTED;
- }
-
- public static Trust fromCode(int code) {
- return trustsByValue.get(code);
- }
-
- public boolean trusted() {
- return this == TRUSTED_X509 || this == TRUSTED;
- }
-
- public boolean trustedInactive() {
- return this == INACTIVE_TRUSTED_X509 || this == INACTIVE_TRUSTED;
- }
- }
-
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) {
this(account, store, remoteAddress);
this.identityKey = identityKey;
@@ -145,79 +72,73 @@ public class XmppAxolotlSession {
this.fresh = false;
}
- protected void setTrust(Trust trust) {
- sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust);
+ protected void setTrust(FingerprintStatus status) {
+ sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status);
}
- protected Trust getTrust() {
- Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint());
- return (trust == null) ? Trust.UNDECIDED : trust;
+ public FingerprintStatus getTrust() {
+ FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint());
+ return (status == null) ? FingerprintStatus.createActiveUndecided() : status;
}
@Nullable
public byte[] processReceiving(byte[] encryptedKey) {
byte[] plaintext = null;
- Trust trust = getTrust();
- switch (trust) {
- case INACTIVE_TRUSTED:
- case UNDECIDED:
- case UNTRUSTED:
- case TRUSTED:
- case INACTIVE_TRUSTED_X509:
- case TRUSTED_X509:
+ FingerprintStatus status = getTrust();
+ if (!status.isCompromised()) {
+ try {
try {
- try {
- PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
- if (!message.getPreKeyId().isPresent()) {
- Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId");
- break;
- }
- Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
- IdentityKey msgIdentityKey = message.getIdentityKey();
- if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) {
- Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint());
- } else {
- this.identityKey = msgIdentityKey;
- plaintext = cipher.decrypt(message);
- preKeyId = message.getPreKeyId().get();
- }
- } catch (InvalidMessageException | InvalidVersionException e) {
- Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
- WhisperMessage message = new WhisperMessage(encryptedKey);
+ PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
+ if (!message.getPreKeyId().isPresent()) {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId");
+ return null;
+ }
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
+ IdentityKey msgIdentityKey = message.getIdentityKey();
+ if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint());
+ } else {
+ this.identityKey = msgIdentityKey;
plaintext = cipher.decrypt(message);
- } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
- Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
+ preKeyId = message.getPreKeyId().get();
}
- } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
+ } catch (InvalidMessageException | InvalidVersionException e) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
+ WhisperMessage message = new WhisperMessage(encryptedKey);
+ plaintext = cipher.decrypt(message);
+ } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
}
+ } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
+ }
- if (plaintext != null) {
- if (trust == Trust.INACTIVE_TRUSTED) {
- setTrust(Trust.TRUSTED);
- } else if (trust == Trust.INACTIVE_TRUSTED_X509) {
- setTrust(Trust.TRUSTED_X509);
- }
+ if (plaintext != null) {
+ if (!status.isActive()) {
+ setTrust(status.toActive());
}
-
- break;
-
- case COMPROMISED:
- default:
- // ignore
- break;
+ }
}
return plaintext;
}
@Nullable
public byte[] processSending(@NonNull byte[] outgoingMessage) {
- Trust trust = getTrust();
- if (trust.trusted()) {
+ FingerprintStatus status = getTrust();
+ if (status.isTrustedAndActive()) {
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
return ciphertextMessage.serialize();
} else {
return null;
}
}
+
+ public Account getAccount() {
+ return account;
+ }
+
+ @Override
+ public int compareTo(XmppAxolotlSession o) {
+ return getTrust().compareTo(o.getTrust());
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index 7d60dcf7..bb89cf17 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -15,16 +15,20 @@ import org.json.JSONObject;
import java.security.PublicKey;
import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@@ -337,6 +341,10 @@ public class Account extends AbstractEntity {
}
}
+ public State getTrueStatus() {
+ return this.status;
+ }
+
public void setStatus(final State status) {
this.status = status;
}
@@ -486,7 +494,7 @@ public class Account extends AbstractEntity {
if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
return null;
}
- this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
+ this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
return this.otrFingerprint;
} catch (final OtrCryptoException ignored) {
return null;
@@ -599,12 +607,44 @@ public class Account extends AbstractEntity {
}
public String getShareableUri() {
- final String fingerprint = this.getOtrFingerprint();
- if (fingerprint != null) {
- return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
+ List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
+ String uri = "xmpp:"+this.getJid().toBareJid().toString();
+ if (fingerprints.size() > 0) {
+ StringBuilder builder = new StringBuilder(uri);
+ builder.append('?');
+ for(int i = 0; i < fingerprints.size(); ++i) {
+ XmppUri.FingerprintType type = fingerprints.get(i).type;
+ if (type == XmppUri.FingerprintType.OMEMO) {
+ builder.append(XmppUri.OMEMO_URI_PARAM);
+ builder.append(fingerprints.get(i).deviceId);
+ } else if (type == XmppUri.FingerprintType.OTR) {
+ builder.append(XmppUri.OTR_URI_PARAM);
+ }
+ builder.append('=');
+ builder.append(fingerprints.get(i).fingerprint);
+ if (i != fingerprints.size() -1) {
+ builder.append(';');
+ }
+ }
+ return builder.toString();
} else {
- return "xmpp:" + this.getJid().toBareJid().toString();
+ return uri;
+ }
+ }
+
+ private List<XmppUri.Fingerprint> getFingerprints() {
+ ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
+ final String otr = this.getOtrFingerprint();
+ if (otr != null) {
+ fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
+ }
+ fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
+ for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
+ if (session.getTrust().isVerified() && session.getTrust().isActive()) {
+ fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
+ }
}
+ return fingerprints;
}
public boolean isBlocked(final ListItem contact) {
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
index 70af45d4..b7307a8b 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -121,7 +121,7 @@ public class Contact implements ListItem, Blockable {
} else if (this.presenceName != null && mutualPresenceSubscription()) {
return this.presenceName;
} else if (jid.hasLocalpart()) {
- return jid.getLocalpart();
+ return jid.getUnescapedLocalpart();
} else {
return jid.getDomainpart();
}
@@ -196,7 +196,7 @@ public class Contact implements ListItem, Blockable {
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
values.put(SERVERNAME, serverName);
- values.put(JID, jid.toString());
+ values.put(JID, jid.toPreppedString());
values.put(OPTIONS, subscription);
values.put(SYSTEMACCOUNT, systemAccount);
values.put(PHOTOURI, photoUri);
@@ -209,10 +209,6 @@ public class Contact implements ListItem, Blockable {
}
}
- public int getSubscription() {
- return this.subscription;
- }
-
public Account getAccount() {
return this.account;
}
@@ -305,7 +301,7 @@ public class Contact implements ListItem, Blockable {
for (int i = 0; i < prints.length(); ++i) {
final String print = prints.isNull(i) ? null : prints.getString(i);
if (print != null && !print.isEmpty()) {
- fingerprints.add(prints.getString(i));
+ fingerprints.add(prints.getString(i).toLowerCase(Locale.US));
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index d9a03fc9..ced48913 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -20,6 +20,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.PgpDecryptionService;
@@ -463,7 +464,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (generatedName != null) {
return generatedName;
} else {
- return getJid().getLocalpart();
+ return getJid().getUnescapedLocalpart();
}
}
} else {
@@ -506,7 +507,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
values.put(NAME, name);
values.put(CONTACT, contactUuid);
values.put(ACCOUNT, accountUuid);
- values.put(CONTACTJID, contactJid.toString());
+ values.put(CONTACTJID, contactJid.toPreppedString());
values.put(CREATED, created);
values.put(STATUS, status);
values.put(MODE, mode);
@@ -627,7 +628,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null;
}
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
- this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
+ this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
return null;
}
@@ -697,36 +698,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public int getNextEncryption() {
- final AxolotlService axolotlService = getAccount().getAxolotlService();
- int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
- if (next == -1) {
- if (Config.supportOmemo()
- && axolotlService != null
- && mode == MODE_SINGLE
- && axolotlService.isConversationAxolotlCapable(this)
- && getAccount().getSelfContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)
- && getContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)) {
- return Message.ENCRYPTION_AXOLOTL;
- } else {
- next = this.getMostRecentlyUsedIncomingEncryption();
- }
- }
-
- if (!Config.supportUnencrypted() && next <= 0) {
- if (Config.supportOmemo()
- && ((axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) || !Config.multipleEncryptionChoices())) {
- return Message.ENCRYPTION_AXOLOTL;
- } else if (Config.supportOtr() && mode == MODE_SINGLE) {
- return Message.ENCRYPTION_OTR;
- } else if (Config.supportOpenPgp()
- && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
- return Message.ENCRYPTION_PGP;
- }
- } else if (next == Message.ENCRYPTION_AXOLOTL
- && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
- next = Message.ENCRYPTION_NONE;
- }
- return next;
+ return Math.max(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, Message.ENCRYPTION_NONE), Message.ENCRYPTION_NONE);
}
public void setNextEncryption(int encryption) {
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index e3577b06..20e4c5a5 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -2,11 +2,13 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
+import android.text.SpannableStringBuilder;
import java.net.MalformedURLException;
import java.net.URL;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper;
@@ -19,8 +21,6 @@ public class Message extends AbstractEntity {
public static final String TABLENAME = "messages";
- public static final String MERGE_SEPARATOR = "\n\u200B\n";
-
public static final int STATUS_RECEIVED = 0;
public static final int STATUS_UNSEND = 1;
public static final int STATUS_SEND = 2;
@@ -59,6 +59,7 @@ public class Message extends AbstractEntity {
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String READ = "read";
+ public static final String ERROR_MESSAGE = "errorMsg";
public static final String ME_COMMAND = "/me ";
@@ -84,6 +85,7 @@ public class Message extends AbstractEntity {
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
+ private String errorMessage = null;
private Message() {
@@ -110,7 +112,8 @@ public class Message extends AbstractEntity {
null,
true,
null,
- false);
+ false,
+ null);
this.conversation = conversation;
}
@@ -119,7 +122,7 @@ public class Message extends AbstractEntity {
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 edited, final boolean oob, final String errorMessage) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@@ -137,6 +140,7 @@ public class Message extends AbstractEntity {
this.read = read;
this.edited = edited;
this.oob = oob;
+ this.errorMessage = errorMessage;
}
public static Message fromCursor(Cursor cursor) {
@@ -178,7 +182,8 @@ public class Message extends AbstractEntity {
cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)),
- cursor.getInt(cursor.getColumnIndex(OOB)) > 0);
+ cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
+ cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@@ -205,12 +210,12 @@ public class Message extends AbstractEntity {
if (counterpart == null) {
values.putNull(COUNTERPART);
} else {
- values.put(COUNTERPART, counterpart.toString());
+ values.put(COUNTERPART, counterpart.toPreppedString());
}
if (trueCounterpart == null) {
values.putNull(TRUE_COUNTERPART);
} else {
- values.put(TRUE_COUNTERPART, trueCounterpart.toString());
+ values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
}
values.put(BODY, body);
values.put(TIME_SENT, timeSent);
@@ -225,6 +230,7 @@ public class Message extends AbstractEntity {
values.put(READ,read ? 1 : 0);
values.put(EDITED, edited);
values.put(OOB, oob ? 1 : 0);
+ values.put(ERROR_MESSAGE,errorMessage);
return values;
}
@@ -272,6 +278,17 @@ public class Message extends AbstractEntity {
this.body = body;
}
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public boolean setErrorMessage(String message) {
+ boolean changed = (message != null && !message.equals(errorMessage))
+ || (message == null && errorMessage != null);
+ this.errorMessage = message;
+ return changed;
+ }
+
public long getTimeSent() {
return timeSent;
}
@@ -476,7 +493,7 @@ public class Message extends AbstractEntity {
!this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() &&
!message.bodyIsHeart() &&
- this.isTrusted() == message.isTrusted()
+ ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint()))
);
}
@@ -491,22 +508,26 @@ public class Message extends AbstractEntity {
);
}
- public String getMergedBody() {
- StringBuilder body = new StringBuilder(this.body.trim());
+ public static class MergeSeparator {}
+
+ public SpannableStringBuilder getMergedBody() {
+ SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
Message current = this;
- while(current.mergeable(current.next())) {
+ while (current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
- body.append(MERGE_SEPARATOR);
+ body.append("\n\n");
+ body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
+ SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
body.append(current.getBody().trim());
}
- return body.toString();
+ return body;
}
public boolean hasMeCommand() {
- return getMergedBody().startsWith(ME_COMMAND);
+ return this.body.trim().startsWith(ME_COMMAND);
}
public int getMergedStatus() {
@@ -592,7 +613,7 @@ public class Message extends AbstractEntity {
if (path == null || path.isEmpty()) {
return null;
}
-
+
String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
int dotPosition = filename.lastIndexOf(".");
@@ -791,8 +812,8 @@ public class Message extends AbstractEntity {
}
public boolean isTrusted() {
- XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
- return t != null && t.trusted();
+ FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
+ return s != null && s.isTrusted();
}
private int getPreviousEncryption() {
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
index fa6afcfa..bbc3e370 100644
--- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -3,7 +3,6 @@ package eu.siacs.conversations.entities;
import android.annotation.SuppressLint;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -395,10 +394,20 @@ public class MucOptions {
if (user != null) {
synchronized (users) {
users.remove(user);
- if (membersOnly() &&
- nonanonymous() &&
- user.affiliation.ranks(Affiliation.MEMBER) &&
- user.realJid != null) {
+ boolean realJidInMuc = false;
+ for (User u : users) {
+ if (user.realJid != null && user.realJid.equals(u.realJid)) {
+ realJidInMuc = true;
+ break;
+ }
+ }
+ boolean self = user.realJid != null && user.realJid.equals(account.getJid().toBareJid());
+ if (membersOnly()
+ && nonanonymous()
+ && user.affiliation.ranks(Affiliation.MEMBER)
+ && user.realJid != null
+ && !realJidInMuc
+ && !self) {
user.role = Role.NONE;
user.avatar = null;
user.fullJid = null;
@@ -409,7 +418,7 @@ public class MucOptions {
return user;
}
- public void addUser(User user) {
+ public void updateUser(User user) {
User old;
if (user.fullJid == null && user.realJid != null) {
old = findUserByRealJid(user.realJid);
@@ -435,7 +444,10 @@ public class MucOptions {
if (old != null) {
users.remove(old);
}
- this.users.add(user);
+ if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
+ && user.getAffiliation().outranks(Affiliation.OUTCAST)){
+ this.users.add(user);
+ }
}
}
@@ -505,8 +517,20 @@ public class MucOptions {
}
public List<User> getUsers(int max) {
- ArrayList<User> users = getUsers();
- return users.subList(0, Math.min(max, users.size()));
+ ArrayList<User> subset = new ArrayList<>();
+ HashSet<Jid> jids = new HashSet<>();
+ jids.add(account.getJid().toBareJid());
+ synchronized (users) {
+ for(User user : users) {
+ if (user.getRealJid() == null || jids.add(user.getRealJid())) {
+ subset.add(user);
+ }
+ if (subset.size() >= max) {
+ break;
+ }
+ }
+ }
+ return subset;
}
public int getUserCount() {
diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
index f9fed914..f8639885 100644
--- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
@@ -38,13 +38,17 @@ public class PresenceGenerator extends AbstractGenerator {
}
public PresencePacket selfPresence(Account account, Presence.Status status) {
+ return selfPresence(account, status, true);
+ }
+
+ public PresencePacket selfPresence(Account account, Presence.Status status, boolean includePgpAnnouncement) {
PresencePacket packet = new PresencePacket();
if(status.toShowString() != null) {
packet.addChild("show").setContent(status.toShowString());
}
packet.setFrom(account.getJid());
String sig = account.getPgpSignature();
- if (sig != null && mXmppConnectionService.getPgpEngine() != null) {
+ if (includePgpAnnouncement && sig != null && mXmppConnectionService.getPgpEngine() != null) {
packet.addChild("x", "jabber:x:signed").setContent(sig);
}
String capHash = getCapHash();
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
index a8b31a7a..18c60bff 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -24,6 +24,7 @@ import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.SSLSocketHelper;
+import eu.siacs.conversations.utils.TLSSocketFactory;
public class HttpConnectionManager extends AbstractConnectionManager {
@@ -77,18 +78,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
new StrictHostnameVerifier());
}
try {
- final SSLContext sc = SSLSocketHelper.getSSLContext();
- sc.init(null, new X509TrustManager[]{trustManager},
- mXmppConnectionService.getRNG());
-
- final SSLSocketFactory sf = sc.getSocketFactory();
- final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
- sf.getSupportedCipherSuites());
- if (cipherSuites.length > 0) {
- sc.getDefaultSSLParameters().setCipherSuites(cipherSuites);
-
- }
-
+ final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
connection.setSSLSocketFactory(sf);
connection.setHostnameVerifier(hostnameVerifier);
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
@@ -96,6 +86,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
}
public Proxy getProxy() throws IOException {
- return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118));
+ return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127,0,0,1}), 8118));
}
}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
index a9bffe3e..87f62706 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
@@ -25,6 +25,7 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.FileWriterException;
public class HttpDownloadConnection implements Transferable {
@@ -141,16 +142,12 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.updateConversationUi();
}
- private class WriteException extends IOException {
-
- }
-
private void showToastForException(Exception e) {
if (e instanceof java.net.UnknownHostException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
} else if (e instanceof java.net.ConnectException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
- } else if (e instanceof WriteException) {
+ } else if (e instanceof FileWriterException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
} else if (!(e instanceof CancellationException)) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
@@ -305,7 +302,7 @@ public class HttpDownloadConnection implements Transferable {
try {
os.write(buffer, 0, count);
} catch (IOException e) {
- throw new WriteException();
+ throw new FileWriterException();
}
updateProgress((int) ((((double) transmitted) / expected) * 100));
if (canceled) {
@@ -315,7 +312,7 @@ public class HttpDownloadConnection implements Transferable {
try {
os.flush();
} catch (IOException e) {
- throw new WriteException();
+ throw new FileWriterException();
}
} catch (CancellationException | IOException e) {
throw e;
diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
index 1bd6a8e4..63a3884b 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
@@ -20,6 +20,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -86,10 +87,10 @@ public class HttpUploadConnection implements Transferable {
this.canceled = true;
}
- private void fail() {
+ private void fail(String errorMessage) {
mHttpConnectionManager.finishUploadConnection(this);
message.setTransferable(null);
- mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
+ mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, errorMessage);
FileBackend.close(mFileInputStream);
}
@@ -111,7 +112,7 @@ public class HttpUploadConnection implements Transferable {
pair = AbstractConnectionManager.createInputStream(file, true);
} catch (FileNotFoundException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not find file to upload - "+e.getMessage());
- fail();
+ fail(e.getMessage());
return;
}
this.file.setExpectedSize(pair.second);
@@ -137,7 +138,7 @@ public class HttpUploadConnection implements Transferable {
}
}
Log.d(Config.LOGTAG,account.getJid().toString()+": invalid response to slot request "+packet);
- fail();
+ fail(IqParser.extractErrorMessage(packet));
}
});
message.setTransferable(this);
@@ -206,12 +207,12 @@ public class HttpUploadConnection implements Transferable {
@Override
public void error(int errorCode, Message object) {
Log.d(Config.LOGTAG,"pgp encryption failed");
- fail();
+ fail("pgp encryption failed");
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
- fail();
+ fail("pgp encryption failed");
}
});
} else {
@@ -219,12 +220,12 @@ public class HttpUploadConnection implements Transferable {
}
} else {
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
- fail();
+ fail("http upload failed because response code was "+code);
}
} catch (IOException e) {
e.printStackTrace();
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
- fail();
+ fail(e.getMessage());
} finally {
FileBackend.close(mFileInputStream);
FileBackend.close(os);
diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
index b548b3c8..b4859e90 100644
--- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -74,22 +74,41 @@ public abstract class AbstractParser {
}
public static MucOptions.User parseItem(Conversation conference, Element item) {
+ return parseItem(conference,item, null);
+ }
+
+ public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid) {
final String local = conference.getJid().getLocalpart();
final String domain = conference.getJid().getDomainpart();
String affiliation = item.getAttribute("affiliation");
String role = item.getAttribute("role");
String nick = item.getAttribute("nick");
- Jid fullJid;
- try {
- fullJid = nick != null ? Jid.fromParts(local, domain, nick) : null;
- } catch (InvalidJidException e) {
- fullJid = null;
+ if (nick != null && fullJid == null) {
+ try {
+ fullJid = Jid.fromParts(local, domain, nick);
+ } catch (InvalidJidException e) {
+ fullJid = null;
+ }
}
Jid realJid = item.getAttributeAsJid("jid");
- MucOptions.User user = new MucOptions.User(conference.getMucOptions(), nick == null ? null : fullJid);
+ MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid);
user.setRealJid(realJid);
user.setAffiliation(affiliation);
user.setRole(role);
return user;
}
+
+ public static String extractErrorMessage(Element packet) {
+ final Element error = packet.findChild("error");
+ if (error != null && error.getChildren().size() > 0) {
+ final String text = error.findChildContent("text");
+ if (text != null && !text.trim().isEmpty()) {
+ return text;
+ } else {
+ return error.getChildren().get(0).getName().replace("-"," ");
+ }
+ } else {
+ return null;
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
index 49b0db21..40eec0d2 100644
--- a/src/main/java/eu/siacs/conversations/parser/IqParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -26,6 +26,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
@@ -319,6 +320,14 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
account.getBlocklist().addAll(jids);
+ if (packet.getType() == IqPacket.TYPE.SET) {
+ for(Jid jid : jids) {
+ Conversation conversation = mXmppConnectionService.find(account,jid);
+ if (conversation != null) {
+ mXmppConnectionService.markRead(conversation);
+ }
+ }
+ }
}
// Update the UI
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 81b68ed9..c8ae1fdc 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -31,6 +31,7 @@ import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
@@ -208,7 +209,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
private static String extractStanzaId(Element packet, Jid by) {
for(Element child : packet.getChildren()) {
if (child.getName().equals("stanza-id")
- && "urn:xmpp:sid:0".equals(child.getNamespace())
+ && Xmlns.STANZA_IDS.equals(child.getNamespace())
&& by.equals(child.getAttributeAsJid("by"))) {
return child.getAttribute("id");
}
@@ -266,19 +267,15 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (packet.getType() == MessagePacket.TYPE_ERROR) {
Jid from = packet.getFrom();
if (from != null) {
- Element error = packet.findChild("error");
- String text = error == null ? null : error.findChildContent("text");
- if (text != null) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
- } else if (error != null) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
- }
Message message = mXmppConnectionService.markMessage(account,
from.toBareJid(),
packet.getId(),
- Message.STATUS_SEND_FAILED);
- if (message != null && message.getEncryption() == Message.ENCRYPTION_OTR) {
- message.getConversation().endOtrIfNeeded();
+ Message.STATUS_SEND_FAILED,
+ extractErrorMessage(packet));
+ if (message != null) {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ message.getConversation().endOtrIfNeeded();
+ }
}
}
return true;
@@ -434,7 +431,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (serverMsgId == null) {
- serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer());
+ final Jid by;
+ final boolean safeToExtract;
+ if (isTypeGroupChat) {
+ by = conversation.getJid().toBareJid();
+ safeToExtract = true; //conversation.getMucOptions().hasFeature(Xmlns.STANZA_IDS);
+ } else {
+ by = account.getJid().toBareJid();
+ safeToExtract = true; //account.getXmppConnection().getFeatures().stanzaIds();
+ }
+ if (safeToExtract) {
+ serverMsgId = extractStanzaId(packet, by);
+ }
}
message.setCounterpart(counterpart);
@@ -604,7 +612,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
+user.getRealJid()+" to "+user.getAffiliation()+" in "
+conversation.getJid().toBareJid());
if (!user.realJidMatchesAccount()) {
- conversation.getMucOptions().addUser(user);
+ conversation.getMucOptions().updateUser(user);
mXmppConnectionService.getAvatarService().clear(conversation);
mXmppConnectionService.updateMucRosterUi();
mXmppConnectionService.updateConversationUi();
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
index 46ce61b7..63bec8bf 100644
--- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -38,7 +38,7 @@ public class PresenceParser extends AbstractParser implements
boolean before = mucOptions.online();
int count = mucOptions.getUserCount();
final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
- processConferencePresence(packet, mucOptions);
+ processConferencePresence(packet, conversation);
final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
if (!tileUserAfter.equals(tileUserBefore)) {
mXmppConnectionService.getAvatarService().clear(mucOptions);
@@ -51,7 +51,8 @@ public class PresenceParser extends AbstractParser implements
}
}
- private void processConferencePresence(PresencePacket packet, MucOptions mucOptions) {
+ private void processConferencePresence(PresencePacket packet, Conversation conversation) {
+ MucOptions mucOptions = conversation.getMucOptions();
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
final String type = packet.getAttribute("type");
@@ -63,10 +64,7 @@ public class PresenceParser extends AbstractParser implements
Element item = x.findChild("item");
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
- MucOptions.User user = new MucOptions.User(mucOptions, from);
- user.setAffiliation(item.getAttribute("affiliation"));
- user.setRole(item.getAttribute("role"));
- user.setRealJid(item.getAttributeAsJid("jid"));
+ MucOptions.User user = parseItem(conversation, item, from);
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
mucOptions.setOnline();
mucOptions.setSelf(user);
@@ -77,7 +75,7 @@ public class PresenceParser extends AbstractParser implements
mucOptions.mNickChangingInProgress = false;
}
} else {
- mucOptions.addUser(user);
+ mucOptions.updateUser(user);
}
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) {
Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().toBareJid()
@@ -131,6 +129,10 @@ public class PresenceParser extends AbstractParser implements
Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
}
} else if (!from.isBareJid()){
+ Element item = x.findChild("item");
+ if (item != null) {
+ mucOptions.updateUser(parseItem(conversation, item, from));
+ }
MucOptions.User user = mucOptions.deleteUser(from);
if (user != null) {
mXmppConnectionService.getAvatarService().clear(user);
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index d8b6b4e1..63d5782b 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -27,17 +27,19 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
-import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@@ -45,7 +47,6 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
-import eu.siacs.conversations.generator.AbstractGenerator;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@@ -54,7 +55,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 28;
+ private static final int DATABASE_VERSION = 33;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -129,7 +130,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ SQLiteAxolotlStore.OWN + " INTEGER, "
+ SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
- + SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ + SQLiteAxolotlStore.TRUST + " TEXT, "
+ + SQLiteAxolotlStore.ACTIVE + " NUMBER, "
+ + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER,"
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
@@ -139,6 +142,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON CONFLICT IGNORE"
+ ");";
+ private static String START_TIMES_TABLE = "start_times";
+
+ private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);";
+
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@@ -181,6 +188,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.EDITED + " TEXT, "
+ Message.READ + " NUMBER DEFAULT 1, "
+ Message.OOB + " INTEGER, "
+ + Message.ERROR_MESSAGE + " TEXT,"
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
@@ -193,6 +201,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
+ db.execSQL(CREATE_START_TIMES_TABLE);
}
@Override
@@ -287,11 +296,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
continue;
}
int ownDeviceId = Integer.valueOf(ownDeviceIdString);
- AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), ownDeviceId);
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownDeviceId);
deleteSession(db, account, ownAddress);
IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
if (identityKeyPair != null) {
- setIdentityKeyTrust(db, account, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.TRUSTED);
+ String[] selectionArgs = {
+ account.getUuid(),
+ identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", "")
+ };
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.TRUSTED, 2);
+ db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
+ selectionArgs);
} else {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair");
}
@@ -333,6 +351,50 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (oldVersion < 28 && newVersion >= 28) {
canonicalizeJids(db);
}
+
+ if (oldVersion < 29 && newVersion >= 29) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
+ }
+ if (oldVersion < 30 && newVersion >= 30) {
+ db.execSQL(CREATE_START_TIMES_TABLE);
+ }
+ if (oldVersion < 31 && newVersion >= 31) {
+ db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT");
+ db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER");
+ HashMap<Integer,ContentValues> migration = new HashMap<>();
+ migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED,true));
+ migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
+ migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
+ migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false));
+ migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
+ migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
+ migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
+ migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true));
+ migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false));
+ for(Map.Entry<Integer,ContentValues> entry : migration.entrySet()) {
+ String whereClause = SQLiteAxolotlStore.TRUSTED+"=?";
+ String[] where = {String.valueOf(entry.getKey())};
+ db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where);
+ }
+
+ }
+ if (oldVersion < 32 && newVersion >= 32) {
+ db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER");
+ ContentValues defaults = new ContentValues();
+ defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis());
+ db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null);
+ }
+ if (oldVersion < 33 && newVersion >= 33) {
+ String whereClause = SQLiteAxolotlStore.OWN+"=1";
+ db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED,true),whereClause,null);
+ }
+ }
+
+ private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.TRUST,trust.toString());
+ values.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
+ return values;
}
private void canonicalizeJids(SQLiteDatabase db) {
@@ -345,7 +407,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
try {
newJid = Jid.fromString(
cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
- ).toString();
+ ).toPreppedString();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
+ cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
@@ -370,7 +432,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
try {
newJid = Jid.fromString(
cursor.getString(cursor.getColumnIndex(Contact.JID))
- ).toString();
+ ).toPreppedString();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
@@ -578,8 +640,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public Conversation findConversation(final Account account, final Jid contactJid) {
SQLiteDatabase db = this.getReadableDatabase();
String[] selectionArgs = {account.getUuid(),
- contactJid.toBareJid().toString() + "/%",
- contactJid.toBareJid().toString()
+ contactJid.toBareJid().toPreppedString() + "/%",
+ contactJid.toBareJid().toPreppedString()
};
Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
@@ -691,7 +753,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.insert(Contact.TABLENAME, null, contact.getContentValues());
} else {
String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
- String[] whereArgs = {account.getUuid(), contact.getJid().toString()};
+ String[] whereArgs = {account.getUuid(), contact.getJid().toPreppedString()};
db.delete(Contact.TABLENAME, where, whereArgs);
}
}
@@ -729,6 +791,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
+ public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
+ String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
+ String[] args = {account.getUuid(), fingerprint};
+ Cursor cursor = getReadableDatabase().rawQuery(SQL,args);
+ long time;
+ if (cursor.moveToFirst()) {
+ time = cursor.getLong(0);
+ } else {
+ time = 0;
+ }
+ cursor.close();
+ return time;
+ }
+
public Pair<Long,String> getLastClearDate(Account account) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {Conversation.ATTRIBUTES};
@@ -993,7 +1069,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
- String[] columns = {SQLiteAxolotlStore.TRUSTED,
+ String[] columns = {SQLiteAxolotlStore.TRUST,
+ SQLiteAxolotlStore.ACTIVE,
+ SQLiteAxolotlStore.LAST_ACTIVATION,
SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid());
@@ -1025,7 +1103,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
- String name = account.getJid().toBareJid().toString();
+ String name = account.getJid().toBareJid().toPreppedString();
IdentityKeyPair identityKeyPair = null;
Cursor cursor = getIdentityKeyCursor(db, account, name, true);
if (cursor.getCount() != 0) {
@@ -1045,18 +1123,21 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return loadIdentityKeys(account, name, null);
}
- public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
+ public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) {
Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false);
while (cursor.moveToNext()) {
- if (trust != null &&
- cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
- != trust.getCode()) {
+ if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) {
continue;
}
try {
- identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0));
+ String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
+ if (key != null) {
+ identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
+ } else {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name);
+ }
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
@@ -1071,22 +1152,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] args = {
account.getUuid(),
name,
- String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()),
- String.valueOf(XmppAxolotlSession.Trust.TRUSTED_X509.getCode())
+ FingerprintStatus.Trust.TRUSTED.toString(),
+ FingerprintStatus.Trust.VERIFIED.toString(),
+ FingerprintStatus.Trust.VERIFIED_X509.toString()
};
return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?"
+ " AND " + SQLiteAxolotlStore.NAME + " = ?"
- + " AND (" + SQLiteAxolotlStore.TRUSTED + " = ? OR " + SQLiteAxolotlStore.TRUSTED + " = ?)",
+ + " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " +SQLiteAxolotlStore.TRUST +" = ?)"
+ + " AND " +SQLiteAxolotlStore.ACTIVE + " > 0",
args
);
}
- private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
- storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
- }
-
- private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
+ private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
@@ -1094,35 +1173,50 @@ public class DatabaseBackend extends SQLiteOpenHelper {
values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(SQLiteAxolotlStore.KEY, base64Serialized);
- values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
+ values.putAll(status.toContentValues());
+ String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?";
+ String[] whereArgs = {account.getUuid(),name,fingerprint};
+ int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs);
+ if (rows == 0) {
+ db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
+ }
+ }
+
+ public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ values.put(SQLiteAxolotlStore.NAME, name);
+ values.put(SQLiteAxolotlStore.OWN, 0);
+ values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
+ values.putAll(status.toContentValues());
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
- public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
+ public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint);
- XmppAxolotlSession.Trust trust = null;
+ final FingerprintStatus status;
if (cursor.getCount() > 0) {
cursor.moveToFirst();
- int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
- trust = XmppAxolotlSession.Trust.fromCode(trustValue);
+ status = FingerprintStatus.fromCursor(cursor);
+ } else {
+ status = null;
}
cursor.close();
- return trust;
+ return status;
}
- public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
+ public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) {
SQLiteDatabase db = this.getWritableDatabase();
- return setIdentityKeyTrust(db, account, fingerprint, trust);
+ return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus);
}
- private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
+ private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) {
String[] selectionArgs = {
account.getUuid(),
fingerprint
};
- ContentValues values = new ContentValues();
- values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
- int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
+ int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(),
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
@@ -1176,12 +1270,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
- public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
- storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
+ public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
+ storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status);
}
public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
- storeIdentityKey(account, account.getJid().toBareJid().toString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
+ storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false));
}
@@ -1217,4 +1311,35 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
}
+
+ public boolean startTimeCountExceedsThreshold() {
+ SQLiteDatabase db = this.getWritableDatabase();
+ long cleanBeforeTimestamp = System.currentTimeMillis() - Config.FREQUENT_RESTARTS_DETECTION_WINDOW;
+ db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp < "+cleanBeforeTimestamp);
+ ContentValues values = new ContentValues();
+ values.put("timestamp",System.currentTimeMillis());
+ db.insert(START_TIMES_TABLE,null,values);
+ String[] columns = new String[]{"count(timestamp)"};
+ Cursor cursor = db.query(START_TIMES_TABLE,columns,null,null,null,null,null);
+ int count;
+ if (cursor.moveToFirst()) {
+ count = cursor.getInt(0);
+ } else {
+ count = 0;
+ }
+ cursor.close();
+ Log.d(Config.LOGTAG,"start time counter reached "+count);
+ return count >= Config.FREQUENT_RESTARTS_THRESHOLD;
+ }
+
+ public void clearStartTimeCounter(boolean justOne) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ if (justOne) {
+ db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp in (select timestamp from "+START_TIMES_TABLE+" order by timestamp desc limit 1)");
+ Log.d(Config.LOGTAG,"do not count start up after being swiped away");
+ } else {
+ Log.d(Config.LOGTAG,"resetting start time counter");
+ db.execSQL("delete from " + START_TIMES_TABLE);
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 921ac12a..84330d16 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -54,12 +54,13 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
- public static final String CONVERSATIONS_FILE_PROVIDER = "eu.siacs.conversations.files";
+ private static final String FILE_PROVIDER = ".files";
private XmppConnectionService mXmppConnectionService;
@@ -250,11 +251,21 @@ public class FileBackend {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
- os.write(buffer, 0, length);
+ try {
+ os.write(buffer, 0, length);
+ } catch (IOException e) {
+ throw new FileWriterException();
+ }
+ }
+ try {
+ os.flush();
+ } catch (IOException e) {
+ throw new FileWriterException();
}
- os.flush();
} catch(FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found);
+ } catch(FileWriterException e) {
+ throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
} catch (IOException e) {
e.printStackTrace();
throw new FileCopyException(R.string.error_io_exception);
@@ -299,8 +310,13 @@ public class FileBackend {
InputStream is = null;
OutputStream os = null;
try {
- file.createNewFile();
+ if (!file.exists() && !file.createNewFile()) {
+ throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
+ }
is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ if (is == null) {
+ throw new FileCopyException(R.string.error_not_an_image_file);
+ }
Bitmap originalBitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
int inSampleSize = (int) Math.pow(2, sampleSize);
@@ -327,7 +343,6 @@ public class FileBackend {
quality -= 5;
}
scaledBitmap.recycle();
- return;
} catch (FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found);
} catch (IOException e) {
@@ -342,8 +357,6 @@ public class FileBackend {
} else {
throw new FileCopyException(R.string.error_out_of_memory);
}
- } catch (NullPointerException e) {
- throw new FileCopyException(R.string.error_io_exception);
} finally {
close(os);
close(is);
@@ -458,12 +471,17 @@ public class FileBackend {
}
file.getParentFile().mkdirs();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
- return FileProvider.getUriForFile(mXmppConnectionService, CONVERSATIONS_FILE_PROVIDER, file);
+ return getUriForFile(mXmppConnectionService,file);
} else {
return Uri.fromFile(file);
}
}
+ public static Uri getUriForFile(Context context, File file) {
+ String packageId = context.getPackageName();
+ return FileProvider.getUriForFile(context, packageId + FILE_PROVIDER, file);
+ }
+
public static Uri getIndexableTakePhotoUri(Uri original) {
if (Config.ONLY_INTERNAL_STORAGE || "file".equals(original.getScheme())) {
return original;
@@ -711,9 +729,7 @@ public class FileBackend {
}
public Uri getJingleFileUri(Message message) {
- File file = getFile(message);
- return FileProvider.getUriForFile(mXmppConnectionService,
- CONVERSATIONS_FILE_PROVIDER, file);
+ return getUriForFile(mXmppConnectionService,getFile(message));
}
public void updateFileParams(Message message) {
diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
index 8d02f975..dfe4cb28 100644
--- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -55,7 +55,7 @@ public class AbstractConnectionManager {
}
public boolean hasStoragePermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (!Config.ONLY_INTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
} else {
return true;
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
index 45a6fd81..4b4d1ed3 100644
--- a/src/main/java/eu/siacs/conversations/services/AvatarService.java
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -44,6 +44,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
}
private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
+ if (contact.isSelf()) {
+ return get(contact.getAccount(),size,cachedOnly);
+ }
final String KEY = key(contact, size);
Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
if (avatar != null || cachedOnly) {
@@ -169,7 +172,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (bitmap != null || cachedOnly) {
return bitmap;
}
- final List<MucOptions.User> users = mucOptions.getUsers();
+ final List<MucOptions.User> users = mucOptions.getUsers(5);
int count = users.size();
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
diff --git a/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java b/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java
new file mode 100644
index 00000000..9c50b081
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java
@@ -0,0 +1,206 @@
+package eu.siacs.conversations.services;
+
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.aztec.AztecWriter;
+import com.google.zxing.common.BitMatrix;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.Hashtable;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class BarcodeProvider extends ContentProvider implements ServiceConnection {
+
+ private static final String AUTHORITY = ".barcodes";
+
+ private final Object lock = new Object();
+
+ private XmppConnectionService mXmppConnectionService;
+ private boolean mBindingInProcess = false;
+
+ @Override
+ public boolean onCreate() {
+ File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/");
+ if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) {
+ for (File file : barcodeDirectory.listFiles()) {
+ if (file.isFile() && !file.isHidden()) {
+ Log.d(Config.LOGTAG, "deleting old barcode file " + file.getAbsolutePath());
+ file.delete();
+ }
+ }
+ }
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getType(Uri uri) {
+ return "image/png";
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ return openFile(uri, mode, null);
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException {
+ Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString());
+ String path = uri.getPath();
+ if (path != null && path.endsWith(".png") && path.length() >= 5) {
+ String jid = path.substring(1).substring(0, path.length() - 4);
+ Log.d(Config.LOGTAG, "account:" + jid);
+ if (connectAndWait()) {
+ Log.d(Config.LOGTAG, "connected to background service");
+ try {
+ Account account = mXmppConnectionService.findAccountByJid(Jid.fromString(jid));
+ if (account != null) {
+ String shareableUri = account.getShareableUri();
+ String hash = CryptoHelper.getFingerprint(shareableUri);
+ File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash);
+ if (!file.exists()) {
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ Bitmap bitmap = createAztecBitmap(account.getShareableUri(), 1024);
+ OutputStream outputStream = new FileOutputStream(file);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
+ outputStream.close();
+ outputStream.flush();
+ }
+ return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+ } catch (Exception e) {
+ throw new FileNotFoundException();
+ }
+ }
+ }
+ throw new FileNotFoundException();
+ }
+
+ private boolean connectAndWait() {
+ Intent intent = new Intent(getContext(), XmppConnectionService.class);
+ intent.setAction(this.getClass().getSimpleName());
+ Context context = getContext();
+ if (context != null) {
+ synchronized (this) {
+ if (mXmppConnectionService == null && !mBindingInProcess) {
+ Log.d(Config.LOGTAG,"calling to bind service");
+ context.startService(intent);
+ context.bindService(intent, this, Context.BIND_AUTO_CREATE);
+ this.mBindingInProcess = true;
+ }
+ }
+ try {
+ waitForService();
+ return true;
+ } catch (InterruptedException e) {
+ return false;
+ }
+ } else {
+ Log.d(Config.LOGTAG, "context was null");
+ return false;
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (this) {
+ XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
+ mXmppConnectionService = binder.getService();
+ mBindingInProcess = false;
+ synchronized (this.lock) {
+ lock.notifyAll();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (this) {
+ mXmppConnectionService = null;
+ }
+ }
+
+ private void waitForService() throws InterruptedException {
+ if (mXmppConnectionService == null) {
+ synchronized (this.lock) {
+ lock.wait();
+ }
+ } else {
+ Log.d(Config.LOGTAG,"not waiting for service because already initialized");
+ }
+ }
+
+ public static Uri getUriForAccount(Context context, Account account) {
+ final String packageId = context.getPackageName();
+ return Uri.parse("content://" + packageId + AUTHORITY + "/" + account.getJid().toBareJid() + ".png");
+ }
+
+ public static Bitmap createAztecBitmap(String input, int size) {
+ try {
+ final AztecWriter AZTEC_WRITER = new AztecWriter();
+ final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
+ hints.put(EncodeHintType.ERROR_CORRECTION, 10);
+ final BitMatrix result = AZTEC_WRITER.encode(input, BarcodeFormat.AZTEC, size, size, hints);
+ final int width = result.getWidth();
+ final int height = result.getHeight();
+ final int[] pixels = new int[width * height];
+ for (int y = 0; y < height; y++) {
+ final int offset = y * width;
+ for (int x = 0; x < width; x++) {
+ pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
+ }
+ }
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
+ return bitmap;
+ } catch (final Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
index ab48435d..9b73035c 100644
--- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java
+++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -19,9 +19,6 @@ public class EventReceiver extends BroadcastReceiver {
mIntentForService.setAction("other");
}
final String action = intent.getAction();
- if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION) && Config.PUSH_MODE) {
- return;
- }
if (action.equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
context.startService(mIntentForService);
}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 6e1d6c4b..47364b30 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -34,6 +34,7 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.TimePreference;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
@@ -591,7 +592,7 @@ public class NotificationService {
errors.add(account);
}
}
- if (mXmppConnectionService.getPreferences().getBoolean("keep_foreground_service", false)) {
+ if (mXmppConnectionService.getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false)) {
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
}
final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 5650b894..b9aeffee 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -65,6 +65,7 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
@@ -92,6 +93,7 @@ import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -102,6 +104,7 @@ import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnBindListener;
import eu.siacs.conversations.xmpp.OnContactStatusChanged;
@@ -145,6 +148,7 @@ public class XmppConnectionService extends Service {
private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
private final IqGenerator mIqGenerator = new IqGenerator(this);
private final List<String> mInProgressAvatarFetches = new ArrayList<>();
+ private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
private long mLastActivity = 0;
@@ -294,6 +298,11 @@ public class XmppConnectionService extends Service {
mOnAccountUpdate.onAccountUpdate();
}
if (account.getStatus() == Account.State.ONLINE) {
+ synchronized (mLowPingTimeoutMode) {
+ if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
+ }
+ }
if (account.setShowErrorNotification(true)) {
databaseBackend.updateAccount(account);
}
@@ -325,32 +334,35 @@ public class XmppConnectionService extends Service {
joinMuc(conversation);
}
account.pendingConferenceJoins.clear();
- scheduleWakeUpCall(Config.PUSH_MODE ? Config.PING_MIN_INTERVAL : Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
- } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
- resetSendingToWaiting(account);
- final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED);
- final boolean listeners = checkListeners();
- final boolean pushMode = Config.PUSH_MODE
- && mPushManagementService.available(account)
- && listeners;
- Log.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode="+Boolean.toString(pushMode)+" listeners="+Boolean.toString(listeners));
- if (!disabled && !pushMode) {
- int timeToReconnect = mRandom.nextInt(20) + 10;
- scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
- }
- } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
- databaseBackend.updateAccount(account);
- reconnectAccount(account, true, false);
- } else if ((account.getStatus() != Account.State.CONNECTING)
- && (account.getStatus() != Account.State.NO_INTERNET)) {
- resetSendingToWaiting(account);
- if (connection != null) {
- int next = connection.getTimeToNextAttempt();
- Log.d(Config.LOGTAG, account.getJid().toBareJid()
- + ": error connecting account. try again in "
- + next + "s for the "
- + (connection.getAttempt() + 1) + " time");
- scheduleWakeUpCall(next, account.getUuid().hashCode());
+ scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
+ } else {
+ if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
+ resetSendingToWaiting(account);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ synchronized (mLowPingTimeoutMode) {
+ if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now");
+ reconnectAccount(account, true, false);
+ } else {
+ int timeToReconnect = mRandom.nextInt(10) + 2;
+ scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
+ }
+ }
+ }
+ } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, true, false);
+ } else if ((account.getStatus() != Account.State.CONNECTING)
+ && (account.getStatus() != Account.State.NO_INTERNET)) {
+ resetSendingToWaiting(account);
+ if (connection != null) {
+ int next = connection.getTimeToNextAttempt();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": error connecting account. try again in "
+ + next + "s for the "
+ + (connection.getAttempt() + 1) + " time");
+ scheduleWakeUpCall(next, account.getUuid().hashCode());
+ }
}
}
getNotificationService().updateErrorNotification();
@@ -537,7 +549,7 @@ public class XmppConnectionService extends Service {
switch (action) {
case ConnectivityManager.CONNECTIVITY_ACTION:
if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
- resetAllAttemptCounts(true);
+ resetAllAttemptCounts(true, false);
}
break;
case ACTION_MERGE_PHONE_CONTACTS:
@@ -556,14 +568,14 @@ public class XmppConnectionService extends Service {
}
break;
case ACTION_DISABLE_FOREGROUND:
- getPreferences().edit().putBoolean("keep_foreground_service", false).commit();
+ getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false).commit();
toggleForegroundService();
break;
case ACTION_DISMISS_ERROR_NOTIFICATIONS:
dismissErrorNotifications();
break;
case ACTION_TRY_AGAIN:
- resetAllAttemptCounts(false);
+ resetAllAttemptCounts(false, true);
interactive = true;
break;
case ACTION_REPLY_TO_CONVERSATION:
@@ -591,8 +603,7 @@ public class XmppConnectionService extends Service {
refreshAllGcmTokens();
break;
case ACTION_IDLE_PING:
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- && !Config.PUSH_MODE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
}
break;
@@ -602,31 +613,58 @@ public class XmppConnectionService extends Service {
break;
}
}
- this.wakeLock.acquire();
+ synchronized (this) {
+ this.wakeLock.acquire();
+ boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action);
+ HashSet<Account> pingCandidates = new HashSet<>();
+ for (Account account : accounts) {
+ pingNow |= processAccountState(account,
+ interactive,
+ "ui".equals(action),
+ CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash),
+ pingCandidates);
+ }
+ if (pingNow) {
+ for (Account account : pingCandidates) {
+ final boolean lowTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid());
+ account.getXmppConnection().sendPing();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")");
+ scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
+ }
+ }
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (final RuntimeException ignored) {
+ }
+ }
+ }
+ return START_STICKY;
+ }
+ private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
boolean pingNow = false;
- HashSet<Account> pingCandidates = new HashSet<>();
-
- for (Account account : accounts) {
- if (!account.isOptionSet(Account.OPTION_DISABLED)) {
- if (!hasInternetConnection()) {
- account.setStatus(Account.State.NO_INTERNET);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!hasInternetConnection()) {
+ account.setStatus(Account.State.NO_INTERNET);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ } else {
+ if (account.getStatus() == Account.State.NO_INTERNET) {
+ account.setStatus(Account.State.OFFLINE);
if (statusListener != null) {
statusListener.onStatusChanged(account);
}
- } else {
- if (account.getStatus() == Account.State.NO_INTERNET) {
- account.setStatus(Account.State.OFFLINE);
- if (statusListener != null) {
- statusListener.onStatusChanged(account);
- }
- }
- if (account.getStatus() == Account.State.ONLINE) {
+ }
+ if (account.getStatus() == Account.State.ONLINE) {
+ synchronized (mLowPingTimeoutMode) {
long lastReceived = account.getXmppConnection().getLastPacketReceived();
long lastSent = account.getXmppConnection().getLastPingSent();
- long pingInterval = (Config.PUSH_MODE || "ui".equals(action)) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
+ long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
- long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime();
+ int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
+ long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
if (lastSent > lastReceived) {
if (pingTimeoutIn < 0) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
@@ -637,62 +675,46 @@ public class XmppConnectionService extends Service {
}
} else {
pingCandidates.add(account);
- if (msToNextPing <= 0 || CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash)) {
+ if (isAccountPushed) {
+ pingNow = true;
+ if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode");
+ }
+ } else if (msToNextPing <= 0) {
pingNow = true;
} else {
this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
+ if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
+ }
}
}
- } else if (account.getStatus() == Account.State.OFFLINE) {
+ }
+ } else if (account.getStatus() == Account.State.OFFLINE) {
+ reconnectAccount(account, true, interactive);
+ } else if (account.getStatus() == Account.State.CONNECTING) {
+ long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
+ long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
+ long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
+ long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
+ if (timeout < 0) {
+ Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast="+secondsSinceLastConnect+")");
+ account.getXmppConnection().resetAttemptCount(false);
reconnectAccount(account, true, interactive);
- } else if (account.getStatus() == Account.State.CONNECTING) {
- long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
- long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
- long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
- long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
- if (timeout < 0) {
- Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
- account.getXmppConnection().resetAttemptCount();
- reconnectAccount(account, true, interactive);
- } else if (discoTimeout < 0) {
- account.getXmppConnection().sendDiscoTimeout();
- scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
- } else {
- scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
- }
+ } else if (discoTimeout < 0) {
+ account.getXmppConnection().sendDiscoTimeout();
+ scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
} else {
- if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
- reconnectAccount(account, true, interactive);
- }
+ scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
}
- }
- if (mOnAccountUpdate != null) {
- mOnAccountUpdate.onAccountUpdate();
- }
- }
- }
- if (pingNow) {
- final boolean listeners = checkListeners();
- for (Account account : pingCandidates) {
- if (listeners
- && Config.PUSH_MODE
- && mPushManagementService.available(account)) {
- account.getXmppConnection().waitForPush();
- cancelWakeUpCall(account.getUuid().hashCode());
} else {
- account.getXmppConnection().sendPing();
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",listeners="+Boolean.toString(listeners)+")");
- scheduleWakeUpCall(Config.PING_TIMEOUT, account.getUuid().hashCode());
+ if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
+ reconnectAccount(account, true, interactive);
+ }
}
}
}
- if (wakeLock.isHeld()) {
- try {
- wakeLock.release();
- } catch (final RuntimeException ignored) {
- }
- }
- return START_STICKY;
+ return pingNow;
}
public boolean isDataSaverDisabled() {
@@ -746,15 +768,15 @@ public class XmppConnectionService extends Service {
}
private boolean manuallyChangePresence() {
- return getPreferences().getBoolean("manually_change_presence", false);
+ return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false);
}
private boolean treatVibrateAsSilent() {
- return getPreferences().getBoolean("treat_vibrate_as_silent", false);
+ return getPreferences().getBoolean(SettingsActivity.TREAT_VIBRATE_AS_SILENT, false);
}
private boolean awayWhenScreenOff() {
- return getPreferences().getBoolean("away_when_screen_off", false);
+ return getPreferences().getBoolean(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, false);
}
private String getCompressPicturesPreference() {
@@ -799,13 +821,13 @@ public class XmppConnectionService extends Service {
}
}
- private void resetAllAttemptCounts(boolean reallyAll) {
+ private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
Log.d(Config.LOGTAG, "resetting all attempt counts");
for (Account account : accounts) {
if (account.hasErrorStatus() || reallyAll) {
final XmppConnection connection = account.getXmppConnection();
if (connection != null) {
- connection.resetAttemptCount();
+ connection.resetAttemptCount(retryImmediately);
}
}
if (account.setShowErrorNotification(true)) {
@@ -852,6 +874,11 @@ public class XmppConnectionService extends Service {
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
+ if (!keepForegroundService() && databaseBackend.startTimeCountExceedsThreshold()) {
+ getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,true).commit();
+ Log.d(Config.LOGTAG,"number of restarts exceeds threshold. enabling foreground service");
+ }
+
restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@@ -882,10 +909,11 @@ public class XmppConnectionService extends Service {
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
+
toggleForegroundService();
updateUnreadCountBadge();
toggleScreenEventReceiver();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Config.PUSH_MODE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -928,17 +956,21 @@ public class XmppConnectionService extends Service {
}
public void toggleForegroundService() {
- if (getPreferences().getBoolean("keep_foreground_service", false)) {
+ if (keepForegroundService()) {
startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
} else {
stopForeground(true);
}
}
+ private boolean keepForegroundService() {
+ return getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,false);
+ }
+
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
- if (!getPreferences().getBoolean("keep_foreground_service", false)) {
+ if (!keepForegroundService()) {
this.logoutAndSave(false);
} else {
Log.d(Config.LOGTAG,"ignoring onTaskRemoved because foreground service is activated");
@@ -947,6 +979,7 @@ public class XmppConnectionService extends Service {
private void logoutAndSave(boolean stop) {
int activeAccounts = 0;
+ databaseBackend.clearStartTimeCounter(true); // regular swipes don't count towards restart counter
for (final Account account : accounts) {
if (account.getStatus() != Account.State.DISABLED) {
activeAccounts++;
@@ -967,13 +1000,6 @@ public class XmppConnectionService extends Service {
}
}
- private void cancelWakeUpCall(int requestCode) {
- final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- final Intent intent = new Intent(this, EventReceiver.class);
- intent.setAction("ping");
- alarmManager.cancel(PendingIntent.getBroadcast(this, requestCode, intent, 0));
- }
-
public void scheduleWakeUpCall(int seconds, int requestCode) {
final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
@@ -997,8 +1023,16 @@ public class XmppConnectionService extends Service {
public XmppConnection createConnection(final Account account) {
final SharedPreferences sharedPref = getPreferences();
- account.setResource(sharedPref.getString("resource", getString(R.string.default_resource))
- .toLowerCase(Locale.getDefault()));
+ String resource;
+ try {
+ resource = sharedPref.getString("resource", getString(R.string.default_resource)).toLowerCase(Locale.ENGLISH);
+ if (resource.trim().isEmpty()) {
+ throw new Exception();
+ }
+ } catch (Exception e) {
+ resource = "conversations";
+ }
+ account.setResource(resource);
final XmppConnection connection = new XmppConnection(account, this);
connection.setOnMessagePacketReceivedListener(this.mMessageParser);
connection.setOnStatusChangedListener(this.statusListener);
@@ -1602,7 +1636,7 @@ public class XmppConnectionService extends Service {
);
}
}
- this.databaseBackend.updateConversation(conversation);
+ updateConversation(conversation);
this.conversations.remove(conversation);
updateConversationUi();
}
@@ -1988,10 +2022,6 @@ public class XmppConnectionService extends Service {
if (connection.getFeatures().csi()) {
connection.sendInactive();
}
- if (Config.PUSH_MODE && mPushManagementService.available(account)) {
- connection.waitForPush();
- cancelWakeUpCall(account.getUuid().hashCode());
- }
}
}
}
@@ -2029,11 +2059,11 @@ public class XmppConnectionService extends Service {
final MucOptions mucOptions = conversation.getMucOptions();
final Jid joinJid = mucOptions.getSelf().getFullJid();
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
- PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE);
+ PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous());
packet.setTo(joinJid);
Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
if (conversation.getMucOptions().getPassword() != null) {
- x.addChild("password").setContent(conversation.getMucOptions().getPassword());
+ x.addChild("password").setContent(mucOptions.getPassword());
}
if (mucOptions.mamSupport()) {
@@ -2101,10 +2131,7 @@ public class XmppConnectionService extends Service {
if ("item".equals(child.getName())) {
MucOptions.User user = AbstractParser.parseItem(conversation,child);
if (!user.realJidMatchesAccount()) {
- conversation.getMucOptions().addUser(user);
- getAvatarService().clear(conversation);
- updateMucRosterUi();
- updateConversationUi();
+ conversation.getMucOptions().updateUser(user);
}
}
}
@@ -2114,6 +2141,9 @@ public class XmppConnectionService extends Service {
++i;
if (i >= affiliations.length) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
+ getAvatarService().clear(conversation);
+ updateMucRosterUi();
+ updateConversationUi();
}
}
};
@@ -2132,7 +2162,7 @@ public class XmppConnectionService extends Service {
}
pushBookmarks(conversation.getAccount());
}
- databaseBackend.updateConversation(conversation);
+ updateConversation(conversation);
joinMuc(conversation);
}
}
@@ -2852,8 +2882,13 @@ public class XmppConnectionService extends Service {
}
}
- public void updateConversation(Conversation conversation) {
- this.databaseBackend.updateConversation(conversation);
+ public void updateConversation(final Conversation conversation) {
+ mDatabaseExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ databaseBackend.updateConversation(conversation);
+ }
+ });
}
private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
@@ -2862,8 +2897,6 @@ public class XmppConnectionService extends Service {
if (connection == null) {
connection = createConnection(account);
account.setXmppConnection(connection);
- } else {
- connection.interrupt();
}
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
if (!force) {
@@ -2872,10 +2905,11 @@ public class XmppConnectionService extends Service {
Thread thread = new Thread(connection);
connection.setInteractive(interactive);
connection.prepareNewConnection();
+ connection.interrupt();
thread.start();
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
} else {
- disconnect(account, force);
+ disconnect(account, force || account.getTrueStatus().isError());
account.getRoster().clearPresences();
connection.resetEverything();
account.getAxolotlService().resetBrokenness();
@@ -2918,6 +2952,10 @@ public class XmppConnectionService extends Service {
}
public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
+ return markMessage(account, recipient, uuid, status, null);
+ }
+
+ public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) {
if (uuid == null) {
return null;
}
@@ -2925,7 +2963,7 @@ public class XmppConnectionService extends Service {
if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
if (message != null) {
- markMessage(message, status);
+ markMessage(message, status, errorMessage);
}
return message;
}
@@ -2948,11 +2986,17 @@ public class XmppConnectionService extends Service {
}
public void markMessage(Message message, int status) {
+ markMessage(message, status, null);
+ }
+
+
+ public void markMessage(Message message, int status, String errorMessage) {
if (status == Message.STATUS_SEND_FAILED
&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
return;
}
+ message.setErrorMessage(errorMessage);
message.setStatus(status);
databaseBackend.updateMessage(message);
updateConversationUi();
@@ -3564,6 +3608,68 @@ public class XmppConnectionService extends Service {
conversation.setBookmark(bookmark);
}
+ public void clearStartTimeCounter() {
+ mDatabaseExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ databaseBackend.clearStartTimeCounter(false);
+ }
+ });
+ }
+
+ public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
+ boolean needsRosterWrite = false;
+ boolean performedVerification = false;
+ final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
+ for(XmppUri.Fingerprint fp : fingerprints) {
+ 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) {
+ if (!fingerprintStatus.isVerified()) {
+ performedVerification = true;
+ axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
+ }
+ } else {
+ axolotlService.preVerifyFingerprint(contact,fingerprint);
+ }
+ }
+ }
+ if (needsRosterWrite) {
+ syncRosterToDisk(contact.getAccount());
+ }
+ return performedVerification;
+ }
+
+ public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
+ final AxolotlService axolotlService = account.getAxolotlService();
+ boolean verifiedSomething = false;
+ for(XmppUri.Fingerprint fp : fingerprints) {
+ if (fp.type == XmppUri.FingerprintType.OMEMO) {
+ String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
+ Log.d(Config.LOGTAG,"trying to verify own fp="+fingerprint);
+ FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
+ if (fingerprintStatus != null) {
+ if (!fingerprintStatus.isVerified()) {
+ axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
+ verifiedSomething = true;
+ }
+ } else {
+ axolotlService.preVerifyFingerprint(account,fingerprint);
+ verifiedSomething = true;
+ }
+ }
+ }
+ return verifiedSomething;
+ }
+
+ public boolean blindTrustBeforeVerification() {
+ return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, true);
+ }
+
public interface OnMamPreferencesFetched {
void onPreferencesFetched(Element prefs);
void onPreferencesFetchFailed();
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index 7791372a..42eb49fb 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -380,7 +380,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
MenuItem invite = menu.findItem(R.id.invite);
startConversation.setVisible(true);
if (contact != null) {
- showContactDetails.setVisible(true);
+ showContactDetails.setVisible(!contact.isSelf());
}
if (user.getRole() == MucOptions.Role.NONE) {
invite.setVisible(true);
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index 08128094..296a10c2 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -36,25 +36,27 @@ import java.security.cert.X509Certificate;
import java.util.List;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.OmemoActivity;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
-public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
+public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
public static final String ACTION_VIEW_CONTACT = "view_contact";
private Contact contact;
@@ -445,14 +447,12 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
}
if (Config.supportOmemo()) {
- for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) {
- boolean highlight = fingerprint.equals(messageFingerprint);
- hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() {
- @Override
- public void onClick(View v) {
- onOmemoKeyClicked(contact.getAccount(), fingerprint);
- }
- });
+ for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) {
+ if (!session.getTrust().isCompromised()) {
+ boolean highlight = session.getFingerprint().equals(messageFingerprint);
+ hasKeys = true;
+ addFingerprintRow(keys, session, highlight);
+ }
}
}
if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
@@ -508,40 +508,6 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
}
- private void onOmemoKeyClicked(Account account, String fingerprint) {
- final XmppAxolotlSession.Trust trust = account.getAxolotlService().getFingerprintTrust(fingerprint);
- if (Config.X509_VERIFICATION && trust != null && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
- X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint);
- if (x509Certificate != null) {
- showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate));
- } else {
- Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show();
- }
- }
- }
-
- private void showCertificateInformationDialog(Bundle bundle) {
- View view = getLayoutInflater().inflate(R.layout.certificate_information, null);
- final String not_available = getString(R.string.certicate_info_not_available);
- TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn);
- TextView subject_o = (TextView) view.findViewById(R.id.subject_o);
- TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn);
- TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o);
- TextView sha1 = (TextView) view.findViewById(R.id.sha1);
-
- subject_cn.setText(bundle.getString("subject_cn", not_available));
- subject_o.setText(bundle.getString("subject_o", not_available));
- issuer_cn.setText(bundle.getString("issuer_cn", not_available));
- issuer_o.setText(bundle.getString("issuer_o", not_available));
- sha1.setText(bundle.getString("sha1", not_available));
-
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setTitle(R.string.certificate_information);
- builder.setView(view);
- builder.setPositiveButton(R.string.ok, null);
- builder.create().show();
- }
-
protected void confirmToDeleteFingerprint(final String fingerprint) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.delete_fingerprint);
@@ -562,15 +528,17 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
builder.create().show();
}
- @Override
public void onBackendConnected() {
- if ((accountJid != null) && (contactJid != null)) {
- Account account = xmppConnectionService
- .findAccountByJid(accountJid);
+ if (accountJid != null && contactJid != null) {
+ Account account = xmppConnectionService.findAccountByJid(accountJid);
if (account == null) {
return;
}
this.contact = account.getRoster().getContact(contactJid);
+ if (mPendingFingerprintVerificationUri != null) {
+ processFingerprintVerification(mPendingFingerprintVerificationUri);
+ mPendingFingerprintVerificationUri = null;
+ }
populateView();
}
}
@@ -579,4 +547,15 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
refreshUi();
}
+
+ @Override
+ protected void processFingerprintVerification(XmppUri uri) {
+ if (contact != null && contact.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+ if (xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints())) {
+ Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
index b2c4efda..d298aa28 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -49,6 +49,7 @@ import de.timroes.android.listview.EnhancedListView;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
@@ -64,6 +65,7 @@ import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@@ -119,6 +121,7 @@ public class ConversationActivity extends XmppActivity
private boolean mActivityPaused = false;
private AtomicBoolean mRedirected = new AtomicBoolean(false);
private Pair<Integer, Intent> mPostponedActivityResult;
+ private boolean mUnprocessedNewIntent = false;
public Conversation getSelectedConversation() {
return this.mSelectedConversation;
@@ -374,7 +377,7 @@ public class ConversationActivity extends XmppActivity
}
public void sendReadMarkerIfNecessary(final Conversation conversation) {
- if (!mActivityPaused && conversation != null) {
+ if (!mActivityPaused && !mUnprocessedNewIntent && conversation != null) {
xmppConnectionService.sendReadMarker(conversation);
}
}
@@ -549,7 +552,7 @@ public class ConversationActivity extends XmppActivity
public void attachFile(final int attachmentChoice) {
if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
- if (!hasStoragePermission(attachmentChoice)) {
+ if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
return;
}
}
@@ -612,10 +615,8 @@ public class ConversationActivity extends XmppActivity
@Override
public void onClick(DialogInterface dialog,
int which) {
- conversation
- .setNextEncryption(Message.ENCRYPTION_NONE);
- xmppConnectionService.databaseBackend
- .updateConversation(conversation);
+ conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+ xmppConnectionService.updateConversation(conversation);
selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE);
}
});
@@ -648,7 +649,7 @@ public class ConversationActivity extends XmppActivity
}
public void startDownloadable(Message message) {
- if (!hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
+ if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
this.mPendingDownloadableMessage = message;
return;
}
@@ -889,7 +890,7 @@ public class ConversationActivity extends XmppActivity
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
}
- xmppConnectionService.databaseBackend.updateConversation(conversation);
+ xmppConnectionService.updateConversation(conversation);
fragment.updateChatMsgHint();
invalidateOptionsMenu();
refreshUi();
@@ -948,8 +949,7 @@ public class ConversationActivity extends XmppActivity
till = System.currentTimeMillis() + (durations[which] * 1000);
}
conversation.setMutedTill(till);
- ConversationActivity.this.xmppConnectionService.databaseBackend
- .updateConversation(conversation);
+ ConversationActivity.this.xmppConnectionService.updateConversation(conversation);
updateConversationList();
ConversationActivity.this.mConversationFragment.updateMessages();
invalidateOptionsMenu();
@@ -960,7 +960,7 @@ public class ConversationActivity extends XmppActivity
public void unmuteConversation(final Conversation conversation) {
conversation.setMutedTill(0);
- this.xmppConnectionService.databaseBackend.updateConversation(conversation);
+ this.xmppConnectionService.updateConversation(conversation);
updateConversationList();
ConversationActivity.this.mConversationFragment.updateMessages();
invalidateOptionsMenu();
@@ -1090,6 +1090,7 @@ public class ConversationActivity extends XmppActivity
protected void onNewIntent(final Intent intent) {
if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
mOpenConversation = null;
+ mUnprocessedNewIntent = true;
if (xmppConnectionServiceBound) {
handleViewConversationIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
@@ -1128,6 +1129,7 @@ public class ConversationActivity extends XmppActivity
}
this.mActivityPaused = false;
+
if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
sendReadMarkerIfNecessary(getSelectedConversation());
}
@@ -1292,6 +1294,7 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.appendText(text);
}
hideConversationsOverview();
+ mUnprocessedNewIntent = false;
openConversation();
if (mContentView instanceof SlidingPaneLayout) {
updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet
@@ -1302,6 +1305,8 @@ public class ConversationActivity extends XmppActivity
startDownloadable(message);
}
}
+ } else {
+ mUnprocessedNewIntent = false;
}
}
@@ -1452,7 +1457,8 @@ public class ConversationActivity extends XmppActivity
}
private long getMaxHttpUploadSize(Conversation conversation) {
- return conversation.getAccount().getXmppConnection().getFeatures().getMaxHttpUploadSize();
+ final XmppConnection connection = conversation.getAccount().getXmppConnection();
+ return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
}
private void setNeverAskForBatteryOptimizationsAgain() {
@@ -1675,8 +1681,8 @@ public class ConversationActivity extends XmppActivity
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
- boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
- boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
+ boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
+ boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 3490a712..d9554024 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -62,6 +62,7 @@ import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
+import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.XmppConnection;
@@ -536,6 +537,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
activity.getMenuInflater().inflate(R.menu.message_context, menu);
menu.setHeaderTitle(R.string.message_options);
MenuItem copyText = menu.findItem(R.id.copy_text);
+ MenuItem selectText = menu.findItem(R.id.select_text);
MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
MenuItem correctMessage = menu.findItem(R.id.correct_message);
MenuItem shareWith = menu.findItem(R.id.share_with);
@@ -544,16 +546,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
MenuItem downloadFile = menu.findItem(R.id.download_file);
MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
MenuItem deleteFile = menu.findItem(R.id.delete_file);
+ MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
if (!treatAsFile
&& !GeoHelper.isGeoUri(m.getBody())
&& m.treatAsDownloadable() != Message.Decision.MUST) {
copyText.setVisible(true);
+ selectText.setVisible(ListSelectionManager.isSupported());
}
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
retryDecryption.setVisible(true);
}
if (relevantForCorrection.getType() == Message.TYPE_TEXT
- && relevantForCorrection.isLastCorrectableMessage()) {
+ && relevantForCorrection.isLastCorrectableMessage()
+ && (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
correctMessage.setVisible(true);
}
if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) {
@@ -586,6 +591,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
}
}
+ if (m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null) {
+ showErrorMessage.setVisible(true);
+ }
}
}
@@ -598,6 +606,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case R.id.copy_text:
copyText(selectedMessage);
return true;
+ case R.id.select_text:
+ selectText(selectedMessage);
+ return true;
case R.id.correct_message:
correctMessage(selectedMessage);
return true;
@@ -619,11 +630,22 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case R.id.delete_file:
deleteFile(selectedMessage);
return true;
+ case R.id.show_error_message:
+ showErrorMessage(selectedMessage);
+ return true;
default:
return super.onContextItemSelected(item);
}
}
+ private void showErrorMessage(final Message message) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.error_message);
+ builder.setMessage(message.getErrorMessage());
+ builder.setPositiveButton(R.string.confirm,null);
+ builder.create().show();
+ }
+
private void shareWith(Message message) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
@@ -632,8 +654,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
shareIntent.setType("text/plain");
} else {
shareIntent.putExtra(Intent.EXTRA_STREAM,
- activity.xmppConnectionService.getFileBackend()
- .getJingleFileUri(message));
+ activity.xmppConnectionService.getFileBackend().getJingleFileUri(message));
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
String mime = message.getMimeType();
if (mime == null) {
@@ -650,13 +671,31 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
private void copyText(Message message) {
- if (activity.copyTextToClipboard(message.getMergedBody(),
+ if (activity.copyTextToClipboard(message.getMergedBody().toString(),
R.string.message_text)) {
Toast.makeText(activity, R.string.message_copied_to_clipboard,
Toast.LENGTH_SHORT).show();
}
}
+ private void selectText(Message message) {
+ final int index;
+ synchronized (this.messageList) {
+ index = this.messageList.indexOf(message);
+ }
+ if (index >= 0) {
+ final int first = this.messagesView.getFirstVisiblePosition();
+ final int last = first + this.messagesView.getChildCount();
+ if (index >= first && index < last) {
+ final View view = this.messagesView.getChildAt(index - first);
+ final TextView messageBody = this.messageListAdapter.getMessageBody(view);
+ if (messageBody != null) {
+ ListSelectionManager.startSelection(messageBody);
+ }
+ }
+ }
+ }
+
private void deleteFile(Message message) {
if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
@@ -877,15 +916,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private void updateSnackBar(final Conversation conversation) {
final Account account = conversation.getAccount();
- final Contact contact = conversation.getContact();
final int mode = conversation.getMode();
+ final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
if (account.getStatus() == Account.State.DISABLED) {
showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
} else if (conversation.isBlocked()) {
showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
- } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ } else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener);
- } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+ } else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription);
} else if (mode == Conversation.MODE_MULTI
&& !conversation.getMucOptions().online()
@@ -1238,8 +1277,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
int which) {
conversation
.setNextEncryption(Message.ENCRYPTION_NONE);
- xmppService.databaseBackend
- .updateConversation(conversation);
+ xmppService.updateConversation(conversation);
message.setEncryption(Message.ENCRYPTION_NONE);
xmppService.sendMessage(message);
messageSent();
@@ -1267,8 +1305,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
conversation
.setNextEncryption(Message.ENCRYPTION_NONE);
message.setEncryption(Message.ENCRYPTION_NONE);
- xmppService.databaseBackend
- .updateConversation(conversation);
+ xmppService.updateConversation(conversation);
xmppService.sendMessage(message);
messageSent();
}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index cc178179..f48e8a48 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -35,23 +35,25 @@ import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
-import android.util.Log;
-
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.OmemoActivity;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.XmppConnection;
@@ -61,20 +63,23 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
-public class EditAccountActivity extends XmppActivity implements OnAccountUpdate,
+public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate,
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
+ private static final int REQUEST_DATA_SAVER = 0x37af244;
private AutoCompleteTextView mAccountJid;
private EditText mPassword;
private EditText mPasswordConfirm;
private CheckBox mRegisterNew;
private Button mCancelButton;
private Button mSaveButton;
- private Button mDisableBatterOptimizations;
+ private Button mDisableOsOptimizationsButton;
+ private TextView mDisableOsOptimizationsHeadline;
+ private TextView getmDisableOsOptimizationsBody;
private TableLayout mMoreTable;
private LinearLayout mStats;
- private RelativeLayout mBatteryOptimizations;
+ private RelativeLayout mOsOptimizations;
private TextView mServerInfoSm;
private TextView mServerInfoRosterVersion;
private TextView mServerInfoCarbons;
@@ -249,6 +254,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TableRow mPushRow;
private String mSavedInstanceAccount;
private boolean mSavedInstanceInit = false;
+ private Button mClearDevicesButton;
public void refreshUiReal() {
invalidateOptionsMenu();
@@ -371,13 +377,24 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
@Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_BATTERY_OP) {
+ if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
updateAccountInformation(mAccount == null);
}
}
+ @Override
+ protected void processFingerprintVerification(XmppUri uri) {
+ if (mAccount != null && mAccount.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+ if (xmppConnectionService.verifyFingerprints(mAccount,uri.getFingerprints())) {
+ Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
+ }
+ }
+
protected void updateSaveButton() {
boolean accountInfoEdited = accountInfoEdited();
@@ -472,21 +489,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mAvatar.setOnClickListener(this.mAvatarClickListener);
this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
this.mStats = (LinearLayout) findViewById(R.id.stats);
- this.mBatteryOptimizations = (RelativeLayout) findViewById(R.id.battery_optimization);
- this.mDisableBatterOptimizations = (Button) findViewById(R.id.batt_op_disable);
- this.mDisableBatterOptimizations.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
- Uri uri = Uri.parse("package:"+getPackageName());
- intent.setData(uri);
- try {
- startActivityForResult(intent, REQUEST_BATTERY_OP);
- } catch (ActivityNotFoundException e) {
- Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
- }
- }
- });
+ this.mOsOptimizations = (RelativeLayout) findViewById(R.id.os_optimization);
+ this.mDisableOsOptimizationsButton = (Button) findViewById(R.id.os_optimization_disable);
+ this.mDisableOsOptimizationsHeadline = (TextView) findViewById(R.id.os_optimization_headline);
+ this.getmDisableOsOptimizationsBody = (TextView) findViewById(R.id.os_optimization_body);
this.mSessionEst = (TextView) findViewById(R.id.session_est);
this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version);
this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
@@ -511,6 +517,13 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mNamePort = (LinearLayout) findViewById(R.id.name_port);
this.mHostname = (EditText) findViewById(R.id.hostname);
this.mHostname.addTextChangedListener(mTextWatcher);
+ this.mClearDevicesButton = (Button) findViewById(R.id.clear_devices);
+ this.mClearDevicesButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showWipePepDialog();
+ }
+ });
this.mPort = (EditText) findViewById(R.id.port);
this.mPort.setText("5222");
this.mPort.addTextChangedListener(mTextWatcher);
@@ -549,12 +562,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
final MenuItem showPassword = menu.findItem(R.id.action_show_password);
- final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
+ final MenuItem share = menu.findItem(R.id.action_share);
renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
+ share.setVisible(mAccount != null && !mInitMode);
+
if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false);
@@ -563,17 +578,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
changePassword.setVisible(false);
}
mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
- Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
- if (otherDevices == null || otherDevices.isEmpty() || !Config.supportOmemo()) {
- clearDevices.setVisible(false);
- }
changePresence.setVisible(manuallyChangePresence());
} else {
showQrCode.setVisible(false);
showBlocklist.setVisible(false);
showMoreInfo.setVisible(false);
changePassword.setVisible(false);
- clearDevices.setVisible(false);
mamPrefs.setVisible(false);
changePresence.setVisible(false);
}
@@ -645,7 +655,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
super.onSaveInstanceState(savedInstanceState);
}
- @Override
protected void onBackendConnected() {
boolean init = true;
if (mSavedInstanceAccount != null) {
@@ -670,6 +679,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mPassword.requestFocus();
}
}
+ if (mPendingFingerprintVerificationUri != null) {
+ processFingerprintVerification(mPendingFingerprintVerificationUri);
+ mPendingFingerprintVerificationUri = null;
+ }
updateAccountInformation(init);
}
@@ -710,15 +723,21 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
case R.id.action_server_info_show_more:
changeMoreTableVisibility(!item.isChecked());
break;
+ case R.id.action_share_barcode:
+ shareBarcode();
+ break;
+ case R.id.action_share_http:
+ shareLink(true);
+ break;
+ case R.id.action_share_uri:
+ shareLink(false);
+ break;
case R.id.action_change_password_on_server:
gotoChangePassword(null);
break;
case R.id.action_mam_prefs:
editMamPrefs();
break;
- case R.id.action_clear_devices:
- showWipePepDialog();
- break;
case R.id.action_renew_certificate:
renewCertificate();
break;
@@ -732,6 +751,27 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
return super.onOptionsItemSelected(item);
}
+ private void shareLink(boolean http) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ String text;
+ if (http) {
+ text = "https://conversations.im/i/"+mAccount.getJid().toBareJid().toString();
+ } else {
+ text = mAccount.getShareableUri();
+ }
+ intent.putExtra(Intent.EXTRA_TEXT,text);
+ startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
+ }
+
+ private void shareBarcode() {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.putExtra(Intent.EXTRA_STREAM,BarcodeProvider.getUriForAccount(this,mAccount));
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setType("image/png");
+ startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
+ }
+
private void changeMoreTableVisibility(boolean visible) {
mMoreTable.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@@ -796,8 +836,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
Features features = this.mAccount.getXmppConnection().getFeatures();
this.mStats.setVisibility(View.VISIBLE);
- boolean showOptimizingWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
- this.mBatteryOptimizations.setVisibility(showOptimizingWarning ? View.VISIBLE : View.GONE);
+ boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
+ boolean showDataSaverWarning = isAffectedByDataSaver();
+ showOsOptimizationWarning(showBatteryWarning,showDataSaverWarning);
this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
.getLastSessionEstablished()));
if (features.rosterVersioning()) {
@@ -893,13 +934,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
-
- if (copyTextToClipboard(ownAxolotlFingerprint.substring(2), R.string.omemo_fingerprint)) {
- Toast.makeText(
- EditAccountActivity.this,
- R.string.toast_message_omemo_fingerprint,
- Toast.LENGTH_SHORT).show();
- }
+ copyOmemoFingerprint(ownAxolotlFingerprint);
}
});
if (Config.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON) {
@@ -919,15 +954,21 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
boolean hasKeys = false;
keys.removeAllViews();
- for (final String fingerprint : mAccount.getAxolotlService().getFingerprintsForOwnSessions()) {
- if (ownAxolotlFingerprint.equals(fingerprint)) {
- continue;
+ for(XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
+ if (!session.getTrust().isCompromised()) {
+ boolean highlight = session.getFingerprint().equals(messageFingerprint);
+ addFingerprintRow(keys,session,highlight);
+ hasKeys = true;
}
- boolean highlight = fingerprint.equals(messageFingerprint);
- hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight, null);
}
if (hasKeys && Config.supportOmemo()) {
keysCard.setVisibility(View.VISIBLE);
+ Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
+ if (otherDevices == null || otherDevices.isEmpty()) {
+ mClearDevicesButton.setVisibility(View.GONE);
+ } else {
+ mClearDevicesButton.setVisibility(View.VISIBLE);
+ }
} else {
keysCard.setVisibility(View.GONE);
}
@@ -956,6 +997,45 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
}
+ private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) {
+ this.mOsOptimizations.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
+ if (showDataSaverWarning) {
+ this.mDisableOsOptimizationsHeadline.setText(R.string.data_saver_enabled);
+ this.getmDisableOsOptimizationsBody.setText(R.string.data_saver_enabled_explained);
+ this.mDisableOsOptimizationsButton.setText(R.string.allow);
+ this.mDisableOsOptimizationsButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
+ Uri uri = Uri.parse("package:"+getPackageName());
+ intent.setData(uri);
+ try {
+ startActivityForResult(intent, REQUEST_DATA_SAVER);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ } else if (showBatteryWarning) {
+ this.mDisableOsOptimizationsButton.setText(R.string.disable);
+ this.mDisableOsOptimizationsHeadline.setText(R.string.battery_optimizations_enabled);
+ this.getmDisableOsOptimizationsBody.setText(R.string.battery_optimizations_enabled_explained);
+ this.mDisableOsOptimizationsButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ Uri uri = Uri.parse("package:"+getPackageName());
+ intent.setData(uri);
+ try {
+ startActivityForResult(intent, REQUEST_BATTERY_OP);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ }
+ }
+
public void showRegenerateAxolotlKeyDialog() {
Builder builder = new Builder(this);
builder.setTitle("Regenerate Key");
diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
index e3841d1d..e609d08e 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java
@@ -1,7 +1,11 @@
package eu.siacs.conversations.ui;
import android.content.Context;
+import android.os.Build;
import android.os.Handler;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Spanned;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.EditText;
@@ -89,4 +93,36 @@ public class EditMessage extends EditText {
boolean onTabPressed(boolean repeated);
}
+ private static final InputFilter SPAN_FILTER = new InputFilter() {
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
+ return source instanceof Spanned ? source.toString() : source;
+ }
+ };
+
+ @Override
+ public boolean onTextContextMenuItem(int id) {
+ if (id == android.R.id.paste) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return super.onTextContextMenuItem(android.R.id.pasteAsPlainText);
+ } else {
+ Editable editable = getEditableText();
+ InputFilter[] filters = editable.getFilters();
+ InputFilter[] tempFilters = new InputFilter[filters != null ? filters.length + 1 : 1];
+ if (filters != null) {
+ System.arraycopy(filters, 0, tempFilters, 1, filters.length);
+ }
+ tempFilters[0] = SPAN_FILTER;
+ editable.setFilters(tempFilters);
+ try {
+ return super.onTextContextMenuItem(id);
+ } finally {
+ editable.setFilters(filters);
+ }
+ }
+ } else {
+ return super.onTextContextMenuItem(id);
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 5dc38a9d..bc80f90a 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -38,6 +38,12 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener {
+ public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
+ public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off";
+ public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
+ public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
+ public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
+
public static final int REQUEST_WRITE_LOGS = 0xbf8701;
private SettingsFragment mSettingsFragment;
@@ -301,10 +307,10 @@ public class SettingsActivity extends XmppActivity implements
final List<String> resendPresence = Arrays.asList(
"confirm_messages",
"xa_on_silent_mode",
- "away_when_screen_off",
+ AWAY_WHEN_SCREEN_IS_OFF,
"allow_message_correction",
- "treat_vibrate_as_silent",
- "manually_change_presence",
+ TREAT_VIBRATE_AS_SILENT,
+ MANUALLY_CHANGE_PRESENCE,
"last_activity");
if (name.equals("resource")) {
String resource = preferences.getString("resource", "mobile")
@@ -322,15 +328,18 @@ public class SettingsActivity extends XmppActivity implements
}
}
}
- } else if (name.equals("keep_foreground_service")) {
+ } else if (name.equals(KEEP_FOREGROUND_SERVICE)) {
+ boolean foreground_service = preferences.getBoolean(KEEP_FOREGROUND_SERVICE,false);
+ if (!foreground_service) {
+ xmppConnectionService.clearStartTimeCounter();
+ }
xmppConnectionService.toggleForegroundService();
} else if (resendPresence.contains(name)) {
if (xmppConnectionServiceBound) {
- if (name.equals("away_when_screen_off")
- || name.equals("manually_change_presence")) {
+ if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) {
xmppConnectionService.toggleScreenEventReceiver();
}
- if (name.equals("manually_change_presence") && !noAccountUsesPgp()) {
+ if (name.equals(MANUALLY_CHANGE_PRESENCE) && !noAccountUsesPgp()) {
Toast.makeText(this, R.string.republish_pgp_keys, Toast.LENGTH_LONG).show();
}
xmppConnectionService.refreshAllPresences();
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
index ec5559ae..25ce50eb 100644
--- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -397,11 +397,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
@SuppressLint("InflateParams")
- protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) {
+ protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
EnterJidDialog dialog = new EnterJidDialog(
this, mKnownHosts, mActivatedAccounts,
getString(R.string.create_contact), getString(R.string.create),
- prefilledJid, null, fingerprint == null
+ prefilledJid, null, invite == null || !invite.hasFingerprints()
);
dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
@@ -420,9 +420,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
if (contact.showInRoster()) {
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
} else {
- contact.addOtrFingerprint(fingerprint);
xmppConnectionService.createContact(contact);
- switchToConversation(contact);
+ if (invite != null && invite.hasFingerprints()) {
+ xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
+ }
+ switchToConversation(contact, invite == null ? null : invite.getBody());
return true;
}
}
@@ -561,11 +563,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
return xmppConnectionService.findAccountByJid(jid);
}
- protected void switchToConversation(Contact contact) {
+ protected void switchToConversation(Contact contact, String body) {
Conversation conversation = xmppConnectionService
.findOrCreateConversation(contact.getAccount(),
contact.getJid(), false);
- switchToConversation(conversation);
+ switchToConversation(conversation, body, false);
}
public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
@@ -624,7 +626,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
showCreateConferenceDialog();
return true;
case R.id.action_scan_qr_code:
- new IntentIntegrator(this).initiateScan();
+ new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
return true;
case R.id.action_hide_offline:
mHideOfflineContacts = !item.isChecked();
@@ -842,28 +844,33 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
private boolean handleJid(Invite invite) {
+ Account account = xmppConnectionService.findAccountByJid(invite.getJid());
+ if (account != null && !account.isOptionSet(Account.OPTION_DISABLED) && invite.hasFingerprints()) {
+ if (xmppConnectionService.verifyFingerprints(account,invite.getFingerprints())) {
+ switchToAccount(account);
+ finish();
+ return true;
+ }
+ }
List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
if (invite.isMuc()) {
Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
if (muc != null) {
- switchToConversation(muc);
+ switchToConversation(muc,invite.getBody(),false);
return true;
} else {
showJoinConferenceDialog(invite.getJid().toBareJid().toString());
return false;
}
} else if (contacts.size() == 0) {
- showCreateContactDialog(invite.getJid().toString(), invite.getFingerprint());
+ showCreateContactDialog(invite.getJid().toString(), invite);
return false;
} else if (contacts.size() == 1) {
Contact contact = contacts.get(0);
- if (invite.getFingerprint() != null) {
- if (contact.addOtrFingerprint(invite.getFingerprint())) {
- Log.d(Config.LOGTAG, "added new fingerprint");
- xmppConnectionService.syncRosterToDisk(contact.getAccount());
- }
+ if (invite.hasFingerprints()) {
+ xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
}
- switchToConversation(contact);
+ switchToConversation(contact,invite.getBody());
return true;
} else {
if (mMenuSearchView != null) {
@@ -1049,10 +1056,14 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
activity.conference_context_id = acmi.position;
} else if (mResContextMenu == R.menu.contact_context) {
activity.contact_context_id = acmi.position;
- final Blockable contact = (Contact) activity.contacts.get(acmi.position);
+ final Contact contact = (Contact) activity.contacts.get(acmi.position);
final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
+ final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
+ if (contact.isSelf()) {
+ showContactDetailsItem.setVisible(false);
+ }
XmppConnection xmpp = contact.getAccount().getXmppConnection();
- if (xmpp != null && xmpp.getFeatures().blocking()) {
+ if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
if (contact.isBlocked()) {
blockUnblockItem.setTitle(R.string.unblock_contact);
} else {
diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
index cc4ba7b2..1f7951e4 100644
--- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
@@ -1,7 +1,12 @@
package eu.siacs.conversations.ui;
+import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -10,24 +15,31 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.OmemoActivity;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
-public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
+public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated {
private List<Jid> contactJids;
private Account mAccount;
@@ -61,6 +73,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
finish();
}
};
+ private Toast mUseCameraHintToast = null;
@Override
protected void refreshUiReal() {
@@ -99,6 +112,61 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
}
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.trust_keys, menu);
+ mUseCameraHintToast = Toast.makeText(this,R.string.use_camera_icon_to_scan_barcode,Toast.LENGTH_LONG);
+ ActionBar actionBar = getActionBar();
+ mUseCameraHintToast.setGravity(Gravity.TOP | Gravity.END, 0 ,actionBar == null ? 0 : actionBar.getHeight());
+ mUseCameraHintToast.show();
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_scan_qr_code:
+ if (hasPendingKeyFetches()) {
+ Toast.makeText(this, R.string.please_wait_for_keys_to_be_fetched, Toast.LENGTH_SHORT).show();
+ } else {
+ new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
+ return true;
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mUseCameraHintToast != null) {
+ mUseCameraHintToast.cancel();
+ }
+ }
+
+ @Override
+ protected void processFingerprintVerification(XmppUri uri) {
+ if (mConversation != null
+ && mAccount != null
+ && uri.hasFingerprints()
+ && mAccount.getAxolotlService().getCryptoTargets(mConversation).contains(uri.getJid())) {
+ boolean performedVerification = xmppConnectionService.verifyFingerprints(mAccount.getRoster().getContact(uri.getJid()),uri.getFingerprints());
+ boolean keys = reloadFingerprints();
+ if (performedVerification && !keys && !hasNoOtherTrustedKeys() && !hasPendingKeyFetches()) {
+ Toast.makeText(this,R.string.all_omemo_keys_have_been_verified, Toast.LENGTH_SHORT).show();
+ finishOk();
+ return;
+ } else if (performedVerification) {
+ Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ reloadFingerprints();
+ Log.d(Config.LOGTAG,"xmpp uri was: "+uri.getJid()+" has Fingerprints: "+Boolean.toString(uri.hasFingerprints()));
+ Toast.makeText(this,R.string.barcode_does_not_contain_fingerprints_for_this_conversation,Toast.LENGTH_SHORT).show();
+ }
+ populateView();
+ }
+
private void populateView() {
setTitle(getString(R.string.trust_omemo_fingerprints));
ownKeys.removeAllViews();
@@ -108,16 +176,14 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for(final String fingerprint : ownKeysToTrust.keySet()) {
hasOwnKeys = true;
addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
- XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)), false,
+ FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)), false, false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ownKeysToTrust.put(fingerprint, isChecked);
// own fingerprints have no impact on locked status.
}
- },
- null,
- null
+ }
);
}
@@ -133,16 +199,14 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
final Map<String, Boolean> fingerprints = entry.getValue();
for (final String fingerprint : fingerprints.keySet()) {
addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
- XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
+ FingerprintStatus.createActive(fingerprints.get(fingerprint)), false, false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
fingerprints.put(fingerprint, isChecked);
lockOrUnlockAsNeeded();
}
- },
- null,
- null
+ }
);
}
if (fingerprints.size() == 0) {
@@ -184,7 +248,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
ownKeysToTrust.clear();
AxolotlService service = this.mAccount.getAxolotlService();
- Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
+ Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided());
for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) {
ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
@@ -193,9 +257,9 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
synchronized (this.foreignKeysToTrust) {
foreignKeysToTrust.clear();
for (Jid jid : contactJids) {
- Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, jid);
+ Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid);
if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
- foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, jid));
+ foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid));
}
Map<String, Boolean> foreignFingerprints = new HashMap<>();
for (final IdentityKey identityKey : foreignKeysSet) {
@@ -211,15 +275,19 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
}
- @Override
public void onBackendConnected() {
Intent intent = getIntent();
this.mAccount = extractAccount(intent);
if (this.mAccount != null && intent != null) {
String uuid = intent.getStringExtra("conversation");
this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
- reloadFingerprints();
- populateView();
+ if (this.mPendingFingerprintVerificationUri != null) {
+ processFingerprintVerification(this.mPendingFingerprintVerificationUri);
+ this.mPendingFingerprintVerificationUri = null;
+ } else {
+ reloadFingerprints();
+ populateView();
+ }
}
}
@@ -238,24 +306,32 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
@Override
public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
+ final boolean keysToTrust = reloadFingerprints();
if (report != null) {
lastFetchReport = report;
runOnUiThread(new Runnable() {
@Override
public void run() {
+ if (mUseCameraHintToast != null && !keysToTrust) {
+ mUseCameraHintToast.cancel();
+ }
switch (report) {
case ERROR:
Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show();
break;
+ case SUCCESS_TRUSTED:
+ Toast.makeText(TrustKeysActivity.this,R.string.blindly_trusted_omemo_keys,Toast.LENGTH_LONG).show();
+ break;
case SUCCESS_VERIFIED:
- Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show();
+ Toast.makeText(TrustKeysActivity.this,
+ Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified,
+ Toast.LENGTH_LONG).show();
break;
}
}
});
}
- boolean keysToTrust = reloadFingerprints();
if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
refreshUi();
} else {
@@ -280,7 +356,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for(final String fingerprint :ownKeysToTrust.keySet()) {
mAccount.getAxolotlService().setFingerprintTrust(
fingerprint,
- XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
+ FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)));
}
List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
synchronized (this.foreignKeysToTrust) {
@@ -293,7 +369,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for (final String fingerprint : value.keySet()) {
mAccount.getAxolotlService().setFingerprintTrust(
fingerprint,
- XmppAxolotlSession.Trust.fromBoolean(value.get(fingerprint)));
+ FingerprintStatus.createActive(value.get(fingerprint)));
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
index d8d02c12..c065bf9f 100644
--- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
@@ -173,11 +173,10 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer
protected boolean verifyWithUri(XmppUri uri) {
Contact contact = mConversation.getContact();
- if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) {
- contact.addOtrFingerprint(uri.getFingerprint());
+ if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+ xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints());
Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show();
updateView();
- xmppConnectionService.syncRosterToDisk(contact.getAccount());
return true;
} else {
Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show();
diff --git a/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java
index 59d58db5..2c9fc131 100644
--- a/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java
@@ -8,22 +8,35 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Button;
+import java.util.List;
+
import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+
+public class WelcomeActivity extends XmppActivity {
+
+ @Override
+ protected void refreshUiReal() {
+
+ }
+
+ @Override
+ void onBackendConnected() {
-public class WelcomeActivity extends Activity {
+ }
@Override
protected void onCreate(final Bundle savedInstanceState) {
if (getResources().getBoolean(R.bool.portrait_only)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.welcome);
final ActionBar ab = getActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(false);
ab.setDisplayHomeAsUpEnabled(false);
}
- super.onCreate(savedInstanceState);
- setContentView(R.layout.welcome);
final Button createAccount = (Button) findViewById(R.id.create_account);
createAccount.setOnClickListener(new View.OnClickListener() {
@Override
@@ -37,7 +50,15 @@ public class WelcomeActivity extends Activity {
useOwnProvider.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- startActivity(new Intent(WelcomeActivity.this, EditAccountActivity.class));
+ List<Account> accounts = xmppConnectionService.getAccounts();
+ Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
+ if (accounts.size() == 1) {
+ intent.putExtra("jid",accounts.get(0).getJid().toBareJid().toString());
+ intent.putExtra("init",true);
+ } else if (accounts.size() >= 1) {
+ intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
+ }
+ startActivity(intent);
}
});
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index e8047ce2..583fab78 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -28,6 +28,7 @@ import android.graphics.Color;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.net.ConnectivityManager;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
@@ -43,24 +44,18 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.util.DisplayMetrics;
-import android.util.Log;
import android.util.Pair;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
-import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
-import com.google.zxing.WriterException;
+import com.google.zxing.aztec.AztecWriter;
import com.google.zxing.common.BitMatrix;
-import com.google.zxing.qrcode.QRCodeWriter;
-import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.java.otr4j.session.SessionID;
@@ -68,7 +63,6 @@ import java.io.FileNotFoundException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
@@ -77,19 +71,16 @@ import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Presences;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
-import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.UIHelper;
@@ -446,6 +437,16 @@ public abstract class XmppActivity extends Activity {
}
}
+ protected boolean isAffectedByDataSaver() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ return cm.isActiveNetworkMetered()
+ && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+ } else {
+ return false;
+ }
+ }
+
protected boolean usingEnterKey() {
return getPreferences().getBoolean("display_enter_key", false);
}
@@ -574,7 +575,7 @@ public abstract class XmppActivity extends Activity {
xmppConnectionService.sendPresence(account);
if (conversation != null) {
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
- xmppConnectionService.databaseBackend.updateConversation(conversation);
+ xmppConnectionService.updateConversation(conversation);
refreshUi();
}
if (onSuccess != null) {
@@ -584,7 +585,14 @@ public abstract class XmppActivity extends Activity {
@Override
public void error(int error, Account account) {
- displayErrorDialog(error);
+ if (error == 0 && account != null) {
+ account.setPgpSignId(0);
+ account.unsetPgpSignature();
+ xmppConnectionService.databaseBackend.updateAccount(account);
+ choosePgpSignId(account);
+ } else {
+ displayErrorDialog(error);
+ }
}
});
}
@@ -761,164 +769,6 @@ public abstract class XmppActivity extends Activity {
builder.create().show();
}
- protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
- final XmppAxolotlSession.Trust trust = account.getAxolotlService()
- .getFingerprintTrust(fingerprint);
- if (trust == null) {
- return false;
- }
- return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
- new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- account.getAxolotlService().setFingerprintTrust(fingerprint,
- (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
- XmppAxolotlSession.Trust.UNTRUSTED);
- }
- },
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- account.getAxolotlService().setFingerprintTrust(fingerprint,
- XmppAxolotlSession.Trust.UNTRUSTED);
- v.setEnabled(true);
- }
- },
- onKeyClickedListener
-
- );
- }
-
- protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
- final String fingerprint,
- boolean highlight,
- XmppAxolotlSession.Trust trust,
- boolean showTag,
- CompoundButton.OnCheckedChangeListener
- onCheckedChangeListener,
- View.OnClickListener onClickListener,
- View.OnClickListener onKeyClickedListener) {
- if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
- return false;
- }
- View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
- TextView key = (TextView) view.findViewById(R.id.key);
- key.setOnClickListener(onKeyClickedListener);
- TextView keyType = (TextView) view.findViewById(R.id.key_type);
- keyType.setOnClickListener(onKeyClickedListener);
- Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
- trustToggle.setVisibility(View.VISIBLE);
- trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
- trustToggle.setOnClickListener(onClickListener);
- final View.OnLongClickListener purge = new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- showPurgeKeyDialog(account, fingerprint);
- return true;
- }
- };
- boolean active = true;
- view.setOnLongClickListener(purge);
- key.setOnLongClickListener(purge);
- keyType.setOnLongClickListener(purge);
- boolean x509 = Config.X509_VERIFICATION
- && (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
- switch (trust) {
- case UNTRUSTED:
- case TRUSTED:
- case TRUSTED_X509:
- trustToggle.setChecked(trust.trusted(), false);
- trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
- if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
- trustToggle.setOnClickListener(null);
- }
- key.setTextColor(getPrimaryTextColor());
- keyType.setTextColor(getSecondaryTextColor());
- break;
- case UNDECIDED:
- trustToggle.setChecked(false, false);
- trustToggle.setEnabled(false);
- key.setTextColor(getPrimaryTextColor());
- keyType.setTextColor(getSecondaryTextColor());
- break;
- case INACTIVE_UNTRUSTED:
- case INACTIVE_UNDECIDED:
- trustToggle.setOnClickListener(null);
- trustToggle.setChecked(false, false);
- trustToggle.setEnabled(false);
- key.setTextColor(getTertiaryTextColor());
- keyType.setTextColor(getTertiaryTextColor());
- active = false;
- break;
- case INACTIVE_TRUSTED:
- case INACTIVE_TRUSTED_X509:
- trustToggle.setOnClickListener(null);
- trustToggle.setChecked(true, false);
- trustToggle.setEnabled(false);
- key.setTextColor(getTertiaryTextColor());
- keyType.setTextColor(getTertiaryTextColor());
- active = false;
- break;
- }
-
- if (showTag) {
- keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
- } else {
- keyType.setVisibility(View.GONE);
- }
- if (highlight) {
- keyType.setTextColor(getResources().getColor(R.color.accent));
- keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
- } else {
- keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
- }
-
- key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
-
- final View.OnClickListener toast;
- if (!active) {
- toast = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
- }
- };
- trustToggle.setOnClickListener(toast);
- } else {
- toast = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- hideToast();
- }
- };
- }
- view.setOnClickListener(toast);
- key.setOnClickListener(toast);
- keyType.setOnClickListener(toast);
-
- keys.addView(view);
- return true;
- }
-
- public void showPurgeKeyDialog(final Account account, final String fingerprint) {
- Builder builder = new Builder(this);
- builder.setTitle(getString(R.string.purge_key));
- builder.setIconAttribute(android.R.attr.alertDialogIcon);
- builder.setMessage(getString(R.string.purge_key_desc_part1)
- + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
- + "\n\n" + getString(R.string.purge_key_desc_part2));
- builder.setNegativeButton(getString(R.string.cancel), null);
- builder.setPositiveButton(getString(R.string.purge_key),
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- account.getAxolotlService().purgeKey(fingerprint);
- refreshUi();
- }
- });
- builder.create().show();
- }
-
public boolean hasStoragePermission(int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
@@ -1142,7 +992,7 @@ public abstract class XmppActivity extends Activity {
}
protected boolean manuallyChangePresence() {
- return getPreferences().getBoolean("manually_change_presence", false);
+ return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false);
}
protected void unregisterNdefPushMessageCallback() {
@@ -1209,7 +1059,7 @@ public abstract class XmppActivity extends Activity {
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
final int width = (size.x < size.y ? size.x : size.y);
- Bitmap bitmap = createQrCodeBitmap(uri, width);
+ Bitmap bitmap = BarcodeProvider.createAztecBitmap(uri, width);
ImageView view = new ImageView(this);
view.setBackgroundColor(Color.WHITE);
view.setImageBitmap(bitmap);
@@ -1219,31 +1069,6 @@ public abstract class XmppActivity extends Activity {
}
}
- protected Bitmap createQrCodeBitmap(String input, int size) {
- Log.d(Config.LOGTAG,"qr code requested size: "+size);
- try {
- final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
- final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
- hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
- final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
- final int width = result.getWidth();
- final int height = result.getHeight();
- final int[] pixels = new int[width * height];
- for (int y = 0; y < height; y++) {
- final int offset = y * width;
- for (int x = 0; x < width; x++) {
- pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
- }
- }
- final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
- bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
- return bitmap;
- } catch (final WriterException e) {
- return null;
- }
- }
-
protected Account extractAccount(Intent intent) {
String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
try {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
index 6a785594..69bc28e5 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -44,6 +44,7 @@ import java.util.regex.Pattern;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
@@ -54,11 +55,13 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
+import eu.siacs.conversations.ui.widget.CopyTextView;
+import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
-public class MessageAdapter extends ArrayAdapter<Message> {
+public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler {
private static final int SENT = 0;
private static final int RECEIVED = 1;
@@ -76,17 +79,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private OnContactPictureClicked mOnContactPictureClickedListener;
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
- private OnLongClickListener openContextMenu = new OnLongClickListener() {
-
- @Override
- public boolean onLongClick(View v) {
- v.showContextMenu();
- return true;
- }
- };
private boolean mIndicateReceived = false;
private boolean mUseGreenBackground = false;
+ private final ListSelectionManager listSelectionManager = new ListSelectionManager();
+
public MessageAdapter(ConversationActivity activity, List<Message> messages) {
super(activity, 0, messages);
this.activity = activity;
@@ -131,7 +128,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
}
- private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
+ private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground, boolean inValidSession) {
String filesize = null;
String info = null;
boolean error = false;
@@ -207,12 +204,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
- XmppAxolotlSession.Trust trust = message.getConversation()
+ FingerprintStatus status = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust(
message.getFingerprint());
- if(trust == null || (!trust.trusted() && !trust.trustedInactive())) {
- viewHolder.indicator.setColorFilter(activity.getWarningTextColor());
+ if(status == null || (type == SENT ? !status.isTrusted() : (!status.isVerified() && inValidSession))) {
+ viewHolder.indicator.setColorFilter(0xffc64545);
viewHolder.indicator.setAlpha(1.0f);
} else {
viewHolder.indicator.clearColorFilter();
@@ -308,32 +305,30 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setIncludeFontPadding(true);
if (message.getBody() != null) {
final String nick = UIHelper.getMessageDisplayName(message);
- String body;
- try {
- body = message.getMergedBody().replaceAll("^" + Message.ME_COMMAND, nick + " ");
- } catch (ArrayIndexOutOfBoundsException e) {
- body = message.getMergedBody();
+ SpannableStringBuilder body = message.getMergedBody();
+ boolean hasMeCommand = message.hasMeCommand();
+ if (hasMeCommand) {
+ body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
}
if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
- body = body.substring(0, Config.MAX_DISPLAY_MESSAGE_CHARS)+"\u2026";
+ body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
+ body.append("\u2026");
}
- Spannable formattedBody = new SpannableString(body);
- int i = body.indexOf(Message.MERGE_SEPARATOR);
- while(i >= 0) {
- final int end = i + Message.MERGE_SEPARATOR.length();
- formattedBody.setSpan(new RelativeSizeSpan(0.3f),i,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- i = body.indexOf(Message.MERGE_SEPARATOR,end);
+ Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
+ for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
+ int start = body.getSpanStart(mergeSeparator);
+ int end = body.getSpanEnd(mergeSeparator);
+ body.setSpan(new RelativeSizeSpan(0.3f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (message.getType() != Message.TYPE_PRIVATE) {
- if (message.hasMeCommand()) {
- formattedBody.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
+ if (hasMeCommand) {
+ body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
String privateMarker;
if (message.getStatus() <= Message.STATUS_RECEIVED) {
- privateMarker = activity
- .getString(R.string.private_message);
+ privateMarker = activity.getString(R.string.private_message);
} else {
final String to;
if (message.getCounterpart() != null) {
@@ -343,25 +338,26 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
privateMarker = activity.getString(R.string.private_message_to, to);
}
- formattedBody = new SpannableStringBuilder().append(privateMarker).append(' ').append(formattedBody);
- formattedBody.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground,false)), 0, privateMarker
- .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- formattedBody.setSpan(new StyleSpan(Typeface.BOLD), 0,
- privateMarker.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- if (message.hasMeCommand()) {
- formattedBody.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarker.length() + 1,
- privateMarker.length() + 1 + nick.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ body.insert(0, privateMarker);
+ int privateMarkerIndex = privateMarker.length();
+ body.insert(privateMarkerIndex, " ");
+ body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)),
+ 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ body.setSpan(new StyleSpan(Typeface.BOLD),
+ 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (hasMeCommand) {
+ body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1,
+ privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
- Linkify.addLinks(formattedBody, Linkify.WEB_URLS);
- Linkify.addLinks(formattedBody, XMPP_PATTERN, "xmpp");
- Linkify.addLinks(formattedBody, GeoHelper.GEO_URI, "geo");
+ Linkify.addLinks(body, Linkify.WEB_URLS);
+ Linkify.addLinks(body, XMPP_PATTERN, "xmpp");
+ Linkify.addLinks(body, GeoHelper.GEO_URI, "geo");
viewHolder.messageBody.setAutoLinkMask(0);
- viewHolder.messageBody.setText(formattedBody);
+ viewHolder.messageBody.setText(body);
viewHolder.messageBody.setTextIsSelectable(true);
viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
+ listSelectionManager.onUpdate(viewHolder.messageBody, message);
} else {
viewHolder.messageBody.setText("");
viewHolder.messageBody.setTextIsSelectable(false);
@@ -370,7 +366,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true));
viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
- viewHolder.messageBody.setOnLongClickListener(openContextMenu);
}
private void displayDownloadableMessage(ViewHolder viewHolder,
@@ -386,7 +381,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
activity.startDownloadable(message);
}
});
- viewHolder.download_button.setOnLongClickListener(openContextMenu);
}
private void displayOpenableMessage(ViewHolder viewHolder,final Message message) {
@@ -401,7 +395,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
openDownloadable(message);
}
});
- viewHolder.download_button.setOnLongClickListener(openContextMenu);
}
private void displayLocationMessage(ViewHolder viewHolder, final Message message) {
@@ -416,7 +409,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
showLocation(message);
}
});
- viewHolder.download_button.setOnLongClickListener(openContextMenu);
}
private void displayImageMessage(ViewHolder viewHolder,
@@ -454,12 +446,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
openDownloadable(message);
}
});
- viewHolder.image.setOnLongClickListener(openContextMenu);
}
private void loadMoreMessages(Conversation conversation) {
conversation.setLastClearHistory(0);
- activity.xmppConnectionService.databaseBackend.updateConversation(conversation);
+ activity.xmppConnectionService.updateConversation(conversation);
conversation.setHasMessagesLeftOnServer(true);
conversation.setFirstMamReference(null);
long timestamp = conversation.getLastMessageTransmitted();
@@ -474,7 +465,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
@Override
public View getView(int position, View view, ViewGroup parent) {
final Message message = getItem(position);
- final boolean isInValidSession = message.isValidInSession();
+ final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
+ final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
final Conversation conversation = message.getConversation();
final Account account = conversation.getAccount();
final int type = getItemViewType(position);
@@ -496,7 +488,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator);
viewHolder.image = (ImageView) view
.findViewById(R.id.message_image);
- viewHolder.messageBody = (TextView) view
+ viewHolder.messageBody = (CopyTextView) view
.findViewById(R.id.message_body);
viewHolder.time = (TextView) view
.findViewById(R.id.message_time);
@@ -517,7 +509,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator);
viewHolder.image = (ImageView) view
.findViewById(R.id.message_image);
- viewHolder.messageBody = (TextView) view
+ viewHolder.messageBody = (CopyTextView) view
.findViewById(R.id.message_body);
viewHolder.time = (TextView) view
.findViewById(R.id.message_time);
@@ -535,6 +527,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder = null;
break;
}
+ if (viewHolder.messageBody != null) {
+ listSelectionManager.onCreate(viewHolder.messageBody);
+ viewHolder.messageBody.setCopyHandler(this);
+ }
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
@@ -676,15 +672,31 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
viewHolder.encryption.setVisibility(View.VISIBLE);
- viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
+ if (omemoEncryption && !message.isTrusted()) {
+ viewHolder.encryption.setText(R.string.not_trusted);
+ } else {
+ viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
+ }
}
}
- displayStatus(viewHolder, message, type, darkBackground);
+ displayStatus(viewHolder, message, type, darkBackground, isInValidSession);
return view;
}
+ @Override
+ public void notifyDataSetChanged() {
+ listSelectionManager.onBeforeNotifyDataSetChanged();
+ super.notifyDataSetChanged();
+ listSelectionManager.onAfterNotifyDataSetChanged();
+ }
+
+ @Override
+ public String transformTextForCopy(CharSequence text, int start, int end) {
+ return text.toString().substring(start, end);
+ }
+
public void openDownloadable(Message message) {
DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
if (!file.exists()) {
@@ -699,7 +711,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
try {
- uri = FileProvider.getUriForFile(activity, FileBackend.CONVERSATIONS_FILE_PROVIDER, file);
+ uri = FileBackend.getUriForFile(activity, file);
} catch (IllegalArgumentException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
@@ -741,6 +753,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
this.mUseGreenBackground = activity.useGreenBackground();
}
+ public TextView getMessageBody(View view) {
+ final Object tag = view.getTag();
+ if (tag instanceof ViewHolder) {
+ final ViewHolder viewHolder = (ViewHolder) tag;
+ return viewHolder.messageBody;
+ }
+ return null;
+ }
+
public interface OnContactPictureClicked {
void onContactPictureClicked(Message message);
}
@@ -757,7 +778,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected ImageView indicator;
protected ImageView indicatorReceived;
protected TextView time;
- protected TextView messageBody;
+ protected CopyTextView messageBody;
protected ImageView contact_picture;
protected TextView status_message;
protected TextView encryption;
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java b/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java
new file mode 100644
index 00000000..bed56192
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java
@@ -0,0 +1,66 @@
+package eu.siacs.conversations.ui.widget;
+
+import android.annotation.TargetApi;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+public class CopyTextView extends TextView {
+
+ public CopyTextView(Context context) {
+ super(context);
+ }
+
+ public CopyTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CopyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @SuppressWarnings("unused")
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public CopyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public interface CopyHandler {
+ public String transformTextForCopy(CharSequence text, int start, int end);
+ }
+
+ private CopyHandler copyHandler;
+
+ public void setCopyHandler(CopyHandler copyHandler) {
+ this.copyHandler = copyHandler;
+ }
+
+ @Override
+ public boolean onTextContextMenuItem(int id) {
+ CharSequence text = getText();
+ int min = 0;
+ int max = text.length();
+ if (isFocused()) {
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+ min = Math.max(0, Math.min(selStart, selEnd));
+ max = Math.max(0, Math.max(selStart, selEnd));
+ }
+ String textForCopy = null;
+ if (id == android.R.id.copy && copyHandler != null) {
+ textForCopy = copyHandler.transformTextForCopy(getText(), min, max);
+ }
+ try {
+ return super.onTextContextMenuItem(id);
+ } finally {
+ if (textForCopy != null) {
+ ClipboardManager clipboard = (ClipboardManager) getContext().
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, textForCopy));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java
new file mode 100644
index 00000000..9e256448
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java
@@ -0,0 +1,201 @@
+package eu.siacs.conversations.ui.widget;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.Selection;
+import android.text.Spannable;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.TextView;
+
+public class ListSelectionManager {
+
+ private static final int MESSAGE_SEND_RESET = 1;
+ private static final int MESSAGE_RESET = 2;
+ private static final int MESSAGE_START_SELECTION = 3;
+
+ private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_SEND_RESET: {
+ // Skip one more message queue loop
+ HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
+ return true;
+ }
+ case MESSAGE_RESET: {
+ final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
+ listSelectionManager.futureSelectionIdentifier = null;
+ return true;
+ }
+ case MESSAGE_START_SELECTION: {
+ final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
+ holder.listSelectionManager.futureSelectionIdentifier = null;
+ startSelection(holder.textView, holder.start, holder.end);
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ private static class StartSelectionHolder {
+
+ public final ListSelectionManager listSelectionManager;
+ public final TextView textView;
+ public final int start;
+ public final int end;
+
+ public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
+ int start, int end) {
+ this.listSelectionManager = listSelectionManager;
+ this.textView = textView;
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ private ActionMode selectionActionMode;
+ private Object selectionIdentifier;
+ private TextView selectionTextView;
+
+ private Object futureSelectionIdentifier;
+ private int futureSelectionStart;
+ private int futureSelectionEnd;
+
+ public void onCreate(TextView textView) {
+ final CustomCallback callback = new CustomCallback(textView);
+ textView.setCustomSelectionActionModeCallback(callback);
+ }
+
+ public void onUpdate(TextView textView, Object identifier) {
+ if (SUPPORTED) {
+ CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
+ callback.identifier = identifier;
+ if (futureSelectionIdentifier == identifier) {
+ HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
+ textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
+ }
+ }
+ }
+
+ public void onBeforeNotifyDataSetChanged() {
+ if (SUPPORTED) {
+ HANDLER.removeMessages(MESSAGE_SEND_RESET);
+ HANDLER.removeMessages(MESSAGE_RESET);
+ HANDLER.removeMessages(MESSAGE_START_SELECTION);
+ if (selectionActionMode != null) {
+ final CharSequence text = selectionTextView.getText();
+ futureSelectionIdentifier = selectionIdentifier;
+ futureSelectionStart = Selection.getSelectionStart(text);
+ futureSelectionEnd = Selection.getSelectionEnd(text);
+ selectionActionMode.finish();
+ selectionActionMode = null;
+ selectionIdentifier = null;
+ selectionTextView = null;
+ }
+ }
+ }
+
+ public void onAfterNotifyDataSetChanged() {
+ if (SUPPORTED && futureSelectionIdentifier != null) {
+ HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
+ }
+ }
+
+ private class CustomCallback implements ActionMode.Callback {
+
+ private final TextView textView;
+ public Object identifier;
+
+ public CustomCallback(TextView textView) {
+ this.textView = textView;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ selectionActionMode = mode;
+ selectionIdentifier = identifier;
+ selectionTextView = textView;
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ if (selectionActionMode == mode) {
+ selectionActionMode = null;
+ selectionIdentifier = null;
+ selectionTextView = null;
+ }
+ }
+ }
+
+ private static final Field FIELD_EDITOR;
+ private static final Method METHOD_START_SELECTION;
+ private static final boolean SUPPORTED;
+
+ static {
+ Field editor;
+ try {
+ editor = TextView.class.getDeclaredField("mEditor");
+ editor.setAccessible(true);
+ } catch (Exception e) {
+ editor = null;
+ }
+ FIELD_EDITOR = editor;
+ Method startSelection = null;
+ if (editor != null) {
+ String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
+ for (String startSelectionName : startSelectionNames) {
+ try {
+ startSelection = editor.getType().getDeclaredMethod(startSelectionName);
+ startSelection.setAccessible(true);
+ break;
+ } catch (Exception e) {
+ startSelection = null;
+ }
+ }
+ }
+ METHOD_START_SELECTION = startSelection;
+ SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
+ }
+
+ public static boolean isSupported() {
+ return SUPPORTED;
+ }
+
+ public static void startSelection(TextView textView) {
+ startSelection(textView, 0, textView.getText().length());
+ }
+
+ public static void startSelection(TextView textView, int start, int end) {
+ final CharSequence text = textView.getText();
+ if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
+ final Spannable spannable = (Spannable) text;
+ start = Math.min(start, spannable.length());
+ end = Math.min(end, spannable.length());
+ Selection.setSelection(spannable, start, end);
+ try {
+ final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
+ METHOD_START_SELECTION.invoke(editor);
+ } catch (Exception e) {
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
index 38ebced1..f1a9d8c4 100644
--- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -206,9 +206,13 @@ public final class CryptoHelper {
}
public static String getAccountFingerprint(Account account) {
+ return getFingerprint(account.getJid().toBareJid().toString());
+ }
+
+ public static String getFingerprint(String value) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
- return bytesToHex(md.digest(account.getJid().toBareJid().toString().getBytes("UTF-8")));
+ return bytesToHex(md.digest(value.getBytes("UTF-8")));
} catch (Exception e) {
return "";
}
diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
index ac64cf2e..a8aba6db 100644
--- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -19,6 +19,7 @@ import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import java.util.Random;
import java.util.TreeMap;
import java.util.Map;
@@ -148,26 +149,29 @@ public class DNSHelper {
for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) {
for (Record rr : rrset) {
Data d = rr.getPayload();
- if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
+ final String name = rr.getName() != null ? rr.getName().toLowerCase(Locale.US) : null;
+ if (d instanceof SRV && NameUtil.idnEquals(qname, name)) {
SRV srv = (SRV) d;
if (!priorities.containsKey(srv.getPriority())) {
priorities.put(srv.getPriority(),new ArrayList<TlsSrv>());
}
priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls));
+ } else if (d instanceof SRV) {
+ Log.d(Config.LOGTAG,"found unrecognized SRV record with name: "+name);
}
if (d instanceof A) {
A a = (A) d;
- if (!ips4.containsKey(rr.getName())) {
- ips4.put(rr.getName(), new ArrayList<String>());
+ if (!ips4.containsKey(name)) {
+ ips4.put(name, new ArrayList<String>());
}
- ips4.get(rr.getName()).add(a.toString());
+ ips4.get(name).add(a.toString());
}
if (d instanceof AAAA) {
AAAA aaaa = (AAAA) d;
- if (!ips6.containsKey(rr.getName())) {
- ips6.put(rr.getName(), new ArrayList<String>());
+ if (!ips6.containsKey(name)) {
+ ips6.put(name, new ArrayList<String>());
}
- ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
+ ips6.get(name).add("[" + aaaa.toString() + "]");
}
}
}
@@ -177,8 +181,8 @@ public class DNSHelper {
Bundle bundle = new Bundle();
try {
client.setTimeout(Config.SOCKET_TIMEOUT * 1000);
- final String qname = "_xmpp-client._tcp." + host;
- final String tlsQname = "_xmpps-client._tcp." + host;
+ final String qname = "_xmpp-client._tcp." + host.toLowerCase(Locale.US);
+ final String tlsQname = "_xmpps-client._tcp." + host.toLowerCase(Locale.US);
Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host);
final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>();
@@ -218,27 +222,28 @@ public class DNSHelper {
}
for (final TlsSrv tlsSrv : result) {
final SRV srv = tlsSrv.srv;
- if (ips6.containsKey(srv.getName())) {
- values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6, tlsSrv.tls));
+ final String name = srv.getName() != null ? srv.getName().toLowerCase(Locale.US) : null;
+ if (ips6.containsKey(name)) {
+ values.add(createNamePortBundle(name,srv.getPort(),ips6, tlsSrv.tls));
} else {
try {
- DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
+ DNSMessage response = client.query(name, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
for (int i = 0; i < response.getAnswers().length; ++i) {
- values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls));
+ values.add(createNamePortBundle(name, srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls));
}
} catch (SocketTimeoutException e) {
Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress());
}
}
- if (ips4.containsKey(srv.getName())) {
- values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4, tlsSrv.tls));
+ if (ips4.containsKey(name)) {
+ values.add(createNamePortBundle(name,srv.getPort(),ips4, tlsSrv.tls));
} else {
- DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress());
+ DNSMessage response = client.query(name, TYPE.A, CLASS.IN, dnsServer.getHostAddress());
for(int i = 0; i < response.getAnswers().length; ++i) {
- values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls));
+ values.add(createNamePortBundle(name,srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls));
}
}
- values.add(createNamePortBundle(srv.getName(), srv.getPort(), tlsSrv.tls));
+ values.add(createNamePortBundle(name, srv.getPort(), tlsSrv.tls));
}
bundle.putParcelableArrayList("values", values);
} catch (SocketTimeoutException e) {
diff --git a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java
new file mode 100644
index 00000000..f406f419
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java
@@ -0,0 +1,4 @@
+package eu.siacs.conversations.utils;
+
+public class FileWriterException extends Exception {
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java
new file mode 100644
index 00000000..9d8a4111
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java
@@ -0,0 +1,69 @@
+package eu.siacs.conversations.utils;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
+public class TLSSocketFactory extends SSLSocketFactory {
+
+ private final SSLSocketFactory internalSSLSocketFactory;
+
+ public TLSSocketFactory(X509TrustManager[] trustManager, SecureRandom random) throws KeyManagementException, NoSuchAlgorithmException {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, trustManager, random);
+ this.internalSSLSocketFactory = context.getSocketFactory();
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return CryptoHelper.getOrderedCipherSuites(internalSSLSocketFactory.getDefaultCipherSuites());
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return internalSSLSocketFactory.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
+ }
+
+ private static Socket enableTLSOnSocket(Socket socket) {
+ if(socket != null && (socket instanceof SSLSocket)) {
+ try {
+ SSLSocketHelper.setSecurity((SSLSocket) socket);
+ } catch (NoSuchAlgorithmException e) {
+ //ignoring
+ }
+ }
+ return socket;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/Xmlns.java b/src/main/java/eu/siacs/conversations/utils/Xmlns.java
index de0a29ce..3e725059 100644
--- a/src/main/java/eu/siacs/conversations/utils/Xmlns.java
+++ b/src/main/java/eu/siacs/conversations/utils/Xmlns.java
@@ -6,4 +6,5 @@ public final class Xmlns {
public static final String REGISTER = "jabber:iq:register";
public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
public static final String HTTP_UPLOAD = "urn:xmpp:http:upload";
+ public static final String STANZA_IDS = "urn:xmpp:sid:0";
}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
index 15a6c9a1..e16377cf 100644
--- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java
+++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
@@ -4,7 +4,9 @@ import android.net.Uri;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@@ -13,7 +15,11 @@ public class XmppUri {
protected String jid;
protected boolean muc;
- protected String fingerprint;
+ protected List<Fingerprint> fingerprints = new ArrayList<>();
+ private String body;
+
+ public static final String OMEMO_URI_PARAM = "omemo-sid-";
+ public static final String OTR_URI_PARAM = "otr-fingerprint";
public XmppUri(String uri) {
try {
@@ -50,13 +56,14 @@ public class XmppUri {
muc = segments.size() > 1 && "j".equalsIgnoreCase(segments.get(0));
} else if ("xmpp".equalsIgnoreCase(scheme)) {
// sample: xmpp:foo@bar.com
- muc = "join".equalsIgnoreCase(uri.getQuery());
+ muc = isMuc(uri.getQuery());
if (uri.getAuthority() != null) {
jid = uri.getAuthority();
} else {
jid = uri.getSchemeSpecificPart().split("\\?")[0];
}
- fingerprint = parseFingerprint(uri.getQuery());
+ this.fingerprints = parseFingerprints(uri.getQuery());
+ this.body = parseBody(uri.getQuery());
} else if ("imto".equalsIgnoreCase(scheme)) {
// sample: imto://xmpp/foo@bar.com
try {
@@ -73,18 +80,52 @@ public class XmppUri {
}
}
- protected String parseFingerprint(String query) {
- if (query == null) {
- return null;
- } else {
- final String NEEDLE = "otr-fingerprint=";
- int index = query.indexOf(NEEDLE);
- if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) {
- return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40);
- } else {
- return null;
+ protected List<Fingerprint> parseFingerprints(String query) {
+ List<Fingerprint> fingerprints = new ArrayList<>();
+ String[] pairs = query == null ? new String[0] : query.split(";");
+ for(String pair : pairs) {
+ String[] parts = pair.split("=",2);
+ if (parts.length == 2) {
+ String key = parts[0].toLowerCase(Locale.US);
+ String value = parts[1].toLowerCase(Locale.US);
+ if (OTR_URI_PARAM.equals(key)) {
+ fingerprints.add(new Fingerprint(FingerprintType.OTR,value));
+ }
+ if (key.startsWith(OMEMO_URI_PARAM)) {
+ try {
+ int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
+ fingerprints.add(new Fingerprint(FingerprintType.OMEMO,value,id));
+ } catch (Exception e) {
+ //ignoring invalid device id
+ }
+ }
}
}
+ return fingerprints;
+ }
+
+ protected String parseBody(String query) {
+ for(String pair : query == null ? new String[0] : query.split(";")) {
+ final String[] parts = pair.split("=",2);
+ if (parts.length == 2 && "body".equals(parts[0].toLowerCase(Locale.US))) {
+ try {
+ return URLDecoder.decode(parts[1],"UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+
+ protected boolean isMuc(String query) {
+ for(String pair : query == null ? new String[0] : query.split(";")) {
+ final String[] parts = pair.split("=",2);
+ if (parts.length == 1 && "join".equals(parts[0])) {
+ return true;
+ }
+ }
+ return false;
}
public Jid getJid() {
@@ -95,7 +136,40 @@ public class XmppUri {
}
}
- public String getFingerprint() {
- return this.fingerprint;
+ public String getBody() {
+ return body;
+ }
+
+ public List<Fingerprint> getFingerprints() {
+ return this.fingerprints;
+ }
+
+ public boolean hasFingerprints() {
+ return fingerprints.size() > 0;
+ }
+ public enum FingerprintType {
+ OMEMO,
+ OTR
+ }
+
+ public static class Fingerprint {
+ public final FingerprintType type;
+ public final String fingerprint;
+ public final int deviceId;
+
+ public Fingerprint(FingerprintType type, String fingerprint) {
+ this(type, fingerprint, 0);
+ }
+
+ public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
+ this.type = type;
+ this.fingerprint = fingerprint;
+ this.deviceId = deviceId;
+ }
+
+ @Override
+ public String toString() {
+ return type.toString()+": "+fingerprint+(deviceId != 0 ? " "+String.valueOf(deviceId) : "");
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 383e990d..128e1187 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -218,7 +218,10 @@ public class XmppConnection implements Runnable {
mXmppConnectionService = service;
}
- protected void changeStatus(final Account.State nextStatus) {
+ protected synchronized void changeStatus(final Account.State nextStatus) {
+ if (Thread.currentThread().isInterrupted()) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not changing status to "+nextStatus+" because thread was interrupted");
+ }
if (account.getStatus() != nextStatus) {
if ((nextStatus == Account.State.OFFLINE)
&& (account.getStatus() != Account.State.CONNECTING)
@@ -262,6 +265,7 @@ public class XmppConnection implements Runnable {
break;
}
try {
+ Socket localSocket;
shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
tagReader = new XmlReader(wakeLock);
tagWriter = new TagWriter();
@@ -276,8 +280,15 @@ public class XmppConnection implements Runnable {
destination = account.getHostname();
}
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via Tor");
- socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
- startXmpp();
+ localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
+ try {
+ startXmpp(localSocket);
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
+ return;
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
+ }
} else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) {
InetSocketAddress address = new InetSocketAddress(account.getHostname(), account.getPort());
@@ -288,33 +299,47 @@ public class XmppConnection implements Runnable {
if (features.encryptionEnabled) {
try {
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
- socket = tlsFactoryVerifier.factory.createSocket();
- socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
- final SSLSession session = ((SSLSocket) socket).getSession();
+ localSocket = tlsFactoryVerifier.factory.createSocket();
+ localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
+ final SSLSession session = ((SSLSocket) localSocket).getSession();
if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), session)) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException();
}
} catch (KeyManagementException e) {
features.encryptionEnabled = false;
- socket = new Socket();
+ localSocket = new Socket();
}
} else {
- socket = new Socket();
- socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
+ localSocket = new Socket();
+ localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
}
} catch (IOException e) {
throw new UnknownHostException();
}
- startXmpp();
+ try {
+ startXmpp(localSocket);
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
+ return;
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
+ }
} else if (DNSHelper.isIp(account.getServer().toString())) {
- socket = new Socket();
+ localSocket = new Socket();
try {
- socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
+ localSocket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
} catch (IOException e) {
throw new UnknownHostException();
}
- startXmpp();
+ try {
+ startXmpp(localSocket);
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
+ return;
+ } catch (Exception e) {
+ throw new IOException(e.getMessage());
+ }
} else {
final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService);
final ArrayList<Parcelable> values = result.getParcelableArrayList("values");
@@ -350,32 +375,37 @@ public class XmppConnection implements Runnable {
}
if (!features.encryptionEnabled) {
- socket = new Socket();
- socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
+ localSocket = new Socket();
+ localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
} else {
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
- socket = tlsFactoryVerifier.factory.createSocket();
+ localSocket = tlsFactoryVerifier.factory.createSocket();
- if (socket == null) {
+ if (localSocket == null) {
throw new IOException("could not initialize ssl socket");
}
- SSLSocketHelper.setSecurity((SSLSocket) socket);
- SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
- SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
+ SSLSocketHelper.setSecurity((SSLSocket) localSocket);
+ SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) localSocket, account.getServer().getDomainpart());
+ SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) localSocket, "xmpp-client");
- socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
+ localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
- if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
+ if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) localSocket).getSession())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException();
}
}
-
- if (startXmpp())
+ if (startXmpp(localSocket)) {
break; // successfully connected to server that speaks xmpp
+ } else {
+ localSocket.close();
+ }
} catch (final SecurityException e) {
throw e;
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
+ return;
} catch (final Throwable e) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")");
if (!iterator.hasNext()) {
@@ -385,8 +415,10 @@ public class XmppConnection implements Runnable {
}
}
processStream();
- } catch (final java.lang.SecurityException e) {
+ } catch (final java.lang.SecurityException e) {
this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
+ } catch (final RegistrationNotSupportedException e) {
+ this.changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
} catch (final IncompatibleServerException e) {
this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
} catch (final SecurityException e) {
@@ -410,12 +442,16 @@ public class XmppConnection implements Runnable {
this.changeStatus(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1);
} finally {
- forceCloseSocket();
- if (wakeLock.isHeld()) {
- try {
- wakeLock.release();
- } catch (final RuntimeException ignored) {
+ if (!Thread.currentThread().isInterrupted()) {
+ forceCloseSocket();
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (final RuntimeException ignored) {
+ }
}
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not force closing socket and releasing wake lock because thread was interrupted");
}
}
}
@@ -423,27 +459,18 @@ public class XmppConnection implements Runnable {
/**
* Starts xmpp protocol, call after connecting to socket
* @return true if server returns with valid xmpp, false otherwise
- * @throws IOException Unknown tag on connect
- * @throws XmlPullParserException Bad Xml
- * @throws NoSuchAlgorithmException Other error
*/
- private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
+ private boolean startXmpp(Socket socket) throws Exception {
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException();
+ }
+ this.socket = socket;
tagWriter.setOutputStream(socket.getOutputStream());
tagReader.setInputStream(socket.getInputStream());
tagWriter.beginDocument();
sendStartStream();
- Tag nextTag;
- while ((nextTag = tagReader.readTag()) != null) {
- if (nextTag.isStart("stream")) {
- return true;
- } else {
- throw new IOException("unknown tag on connect");
- }
- }
- if (socket.isConnected()) {
- socket.close();
- }
- return false;
+ final Tag tag = tagReader.readTag();
+ return tag != null && tag.isStart("stream");
}
private static class TlsFactoryVerifier {
@@ -812,10 +839,8 @@ public class XmppConnection implements Runnable {
} else {
throw new IncompatibleServerException();
}
- } else if (!this.streamFeatures.hasChild("register")
- && account.isOptionSet(Account.OPTION_REGISTER)) {
- forceCloseSocket();
- changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
+ } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
+ throw new RegistrationNotSupportedException();
} else if (this.streamFeatures.hasChild("mechanisms")
&& shouldAuthenticate
&& (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
@@ -950,7 +975,7 @@ public class XmppConnection implements Runnable {
}
public void resetEverything() {
- resetAttemptCount();
+ resetAttemptCount(true);
resetStreamId();
clearIqCallbacks();
mStanzaQueue.clear();
@@ -1359,30 +1384,6 @@ public class XmppConnection implements Runnable {
}
}
- public void waitForPush() {
- if (tagWriter.isActive()) {
- tagWriter.finish();
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- while(!tagWriter.finished()) {
- Thread.sleep(10);
- }
- socket.close();
- Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream");
- changeStatus(Account.State.OFFLINE);
- } catch (IOException | InterruptedException e) {
- Log.d(Config.LOGTAG,account.getJid().toBareJid()+": error while closing socket for waitForPush()");
- }
- }
- }).start();
- } else {
- forceCloseSocket();
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)");
- }
- }
-
private void forceCloseSocket() {
if (socket != null) {
try {
@@ -1526,9 +1527,11 @@ public class XmppConnection implements Runnable {
this.sendPacket(new InactivePacket());
}
- public void resetAttemptCount() {
+ public void resetAttemptCount(boolean resetConnectTime) {
this.attempt = 0;
- this.lastConnect = 0;
+ if (resetConnectTime) {
+ this.lastConnect = 0;
+ }
}
public void setInteractive(boolean interactive) {
@@ -1567,6 +1570,10 @@ public class XmppConnection implements Runnable {
}
+ private class RegistrationNotSupportedException extends IOException {
+
+ }
+
public enum Identity {
FACEBOOK,
SLACK,
@@ -1685,6 +1692,10 @@ public class XmppConnection implements Runnable {
return -1;
}
}
+
+ public boolean stanzaIds() {
+ return hasDiscoFeature(account.getJid().toBareJid(),Xmlns.STANZA_IDS);
+ }
}
private IqGenerator getIqGenerator() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
index 6430d41e..f551dd20 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java
@@ -21,10 +21,28 @@ public final class Jid {
private final String domainpart;
private final String resourcepart;
+ private static final char[] JID_ESCAPING_CHARS = {' ','"','&','\'','/',':','<','>','@','\\'};
+
+ // It's much more efficient to store the ful JID as well as the parts instead of figuring them
+ // all out every time (since some characters are displayed but aren't used for comparisons).
+ private final String displayjid;
+
public String getLocalpart() {
return localpart;
}
+ public String getUnescapedLocalpart() {
+ if (localpart == null || !localpart.contains("\\")) {
+ return localpart;
+ } else {
+ String localpart = this.localpart;
+ for(char c : JID_ESCAPING_CHARS) {
+ localpart = localpart.replace(String.format ("\\%02x", (int)c),String.valueOf(c));
+ }
+ return localpart;
+ }
+ }
+
public String getDomainpart() {
return IDN.toUnicode(domainpart);
}
@@ -69,6 +87,7 @@ public final class Jid {
Jid fromCache = Jid.cache.get(jid);
if (fromCache != null) {
+ displayjid = fromCache.displayjid;
localpart = fromCache.localpart;
domainpart = fromCache.domainpart;
resourcepart = fromCache.resourcepart;
@@ -89,6 +108,8 @@ public final class Jid {
throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER);
}
+ String finaljid;
+
final int domainpartStart;
final int atLoc = jid.indexOf("@");
final int slashLoc = jid.indexOf("/");
@@ -96,6 +117,7 @@ public final class Jid {
// or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
localpart = "";
+ finaljid = "";
domainpartStart = 0;
} else {
final String lp = jid.substring(0, atLoc);
@@ -108,6 +130,7 @@ public final class Jid {
throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
}
domainpartStart = atLoc + 1;
+ finaljid = lp + "@";
}
final String dp;
@@ -126,6 +149,7 @@ public final class Jid {
} catch (final StringprepException e) {
throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
}
+ finaljid = finaljid + dp + "/" + rp;
} else {
resourcepart = "";
try{
@@ -133,6 +157,7 @@ public final class Jid {
} catch (final StringprepException e) {
throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
}
+ finaljid = finaljid + dp;
}
// Remove trailing "." before storing the domain part.
@@ -156,6 +181,8 @@ public final class Jid {
}
Jid.cache.put(jid, this);
+
+ this.displayjid = finaljid;
}
public Jid toBareJid() {
@@ -178,6 +205,10 @@ public final class Jid {
@Override
public String toString() {
+ return displayjid;
+ }
+
+ public String toPreppedString() {
String out;
if (hasLocalpart()) {
out = localpart + '@' + domainpart;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
index 3099b570..5461b9c6 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -25,6 +25,7 @@ import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -87,7 +88,7 @@ public class JingleConnection implements Transferable {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() != IqPacket.TYPE.RESULT) {
- fail();
+ fail(IqParser.extractErrorMessage(packet));
}
}
};
@@ -488,7 +489,7 @@ public class JingleConnection implements Transferable {
mJingleStatus = JINGLE_STATUS_INITIATED;
mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
} else {
- fail();
+ fail(IqParser.extractErrorMessage(packet));
}
}
});
@@ -619,6 +620,10 @@ public class JingleConnection implements Transferable {
if (cid != null) {
Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
JingleCandidate candidate = getCandidate(cid);
+ if (candidate == null) {
+ Log.d(Config.LOGTAG,"could not find candidate with cid="+cid);
+ return false;
+ }
candidate.flagAsUsedByCounterpart();
this.receivedCandidate = true;
if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
@@ -846,6 +851,10 @@ public class JingleConnection implements Transferable {
}
private void fail() {
+ fail(null);
+ }
+
+ private void fail(String errorMessage) {
this.mJingleStatus = JINGLE_STATUS_FAILED;
this.disconnectSocks5Connections();
if (this.transport != null && this.transport instanceof JingleInbandTransport) {
@@ -862,7 +871,8 @@ public class JingleConnection implements Transferable {
this.mXmppConnectionService.updateConversationUi();
} else {
this.mXmppConnectionService.markMessage(this.message,
- Message.STATUS_SEND_FAILED);
+ Message.STATUS_SEND_FAILED,
+ errorMessage);
this.message.setTransferable(null);
}
}