diff options
Diffstat (limited to '')
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); } } |