diff options
Diffstat (limited to 'src')
191 files changed, 6369 insertions, 1917 deletions
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9fe37017..7f07a39c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -15,6 +15,8 @@ <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.NFC" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" /> + <application android:allowBackup="true" android:icon="@drawable/ic_launcher" @@ -131,6 +133,10 @@ </intent-filter> </activity> <activity + android:name=".ui.TrustKeysActivity" + android:label="@string/trust_omemo_fingerprints" + android:windowSoftInputMode="stateAlwaysHidden"/> + <activity android:name="de.duenndns.ssl.MemorizingActivity" android:theme="@style/ConversationsTheme" tools:replace="android:theme"/> diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 00225f7b..f2303626 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -8,6 +8,11 @@ public final class Config { public static final String LOGTAG = "conversations"; + + public static final String DOMAIN_LOCK = null; //only allow account creation for this domain + public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox + public static final boolean HIDE_PGP_IN_UI = false; //some more consumer focused clients might want to disable OpenPGP + public static final int PING_MAX_INTERVAL = 300; public static final int PING_MIN_INTERVAL = 30; public static final int PING_TIMEOUT = 10; @@ -19,6 +24,10 @@ public final class Config { public static final int AVATAR_SIZE = 192; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; + public static final int IMAGE_SIZE = 1920; + public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG; + public static final int IMAGE_QUALITY = 75; + public static final int MESSAGE_MERGE_WINDOW = 20; public static final boolean UTF8_EMOTICONS = false; @@ -26,7 +35,6 @@ public final class Config { public static final int PAGE_SIZE = 50; public static final int MAX_NUM_PAGES = 3; - public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; public static final int REFRESH_UI_INTERVAL = 500; public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb @@ -36,6 +44,10 @@ public final class Config { public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false; + public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true; + + public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false; + 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; diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java index c23a7fc8..1b296cc4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrService.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java @@ -1,5 +1,20 @@ package eu.siacs.conversations.crypto; +import android.util.Log; + +import net.java.otr4j.OtrEngineHost; +import net.java.otr4j.OtrException; +import net.java.otr4j.OtrPolicy; +import net.java.otr4j.OtrPolicyImpl; +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.FragmenterInstructions; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; + +import org.json.JSONException; +import org.json.JSONObject; + import java.math.BigInteger; import java.security.KeyFactory; import java.security.KeyPair; @@ -11,31 +26,15 @@ import java.security.spec.DSAPrivateKeySpec; import java.security.spec.DSAPublicKeySpec; import java.security.spec.InvalidKeySpecException; -import org.json.JSONException; -import org.json.JSONObject; - -import android.util.Log; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import net.java.otr4j.OtrEngineHost; -import net.java.otr4j.OtrException; -import net.java.otr4j.OtrPolicy; -import net.java.otr4j.OtrPolicyImpl; -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.InstanceTag; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.FragmenterInstructions; - public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { private Account account; diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index f393264c..33089a8c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -1,5 +1,13 @@ package eu.siacs.conversations.crypto; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; @@ -9,10 +17,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URL; -import org.openintents.openpgp.OpenPgpSignatureResult; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; - import de.tzur.conversations.Settings; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -23,9 +27,6 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.UiCallback; -import android.app.PendingIntent; -import android.content.Intent; -import android.net.Uri; public class PgpEngine { private OpenPgpApi api; diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java new file mode 100644 index 00000000..d700b644 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -0,0 +1,728 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.SessionBuilder; +import org.whispersystems.libaxolotl.UntrustedIdentityException; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyBundle; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.security.Security; +import java.util.Arrays; +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 eu.siacs.conversations.Config; +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.parser.IqParser; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class AxolotlService { + + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + + public static final String LOGPREFIX = "AxolotlService"; + + public static final int NUM_KEYS_TO_PUBLISH = 100; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private final Map<Jid, Set<Integer>> deviceIds; + private final Map<String, XmppAxolotlMessage> messageCache; + private final FetchStatusMap fetchStatusMap; + private final SerialSingleThreadExecutor executor; + + private static class AxolotlAddressMap<T> { + protected Map<String, Map<Integer, T>> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map<Integer, T> getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + public void clear() { + map.clear(); + } + + } + + private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> { + private final XmppConnectionService xmppConnectionService; + private final Account account; + + public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { + super(); + this.xmppConnectionService = service; + this.account = account; + this.fillMap(store); + } + + 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()); + String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", ""); + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint)); + } + } + + private void fillMap(SQLiteAxolotlStore store) { + List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString()); + putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store); + for (Contact contact : account.getRoster().getContacts()) { + Jid bareJid = contact.getJid().toBareJid(); + if (bareJid == null) { + continue; // FIXME: handle this? + } + String address = bareJid.toString(); + deviceIds = store.getSubDeviceSessions(address); + putDevicesForJid(address, deviceIds, store); + } + + } + + @Override + public void put(AxolotlAddress address, XmppAxolotlSession value) { + super.put(address, value); + value.setNotFresh(); + xmppConnectionService.syncRosterToDisk(account); + } + + public void put(XmppAxolotlSession session) { + this.put(session.getRemoteAddress(), session); + } + } + + private static enum FetchStatus { + PENDING, + SUCCESS, + ERROR + } + + private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> { + + } + + public static String getLogprefix(Account account) { + return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; + } + + public AxolotlService(Account account, XmppConnectionService connectionService) { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.deviceIds = new HashMap<>(); + this.messageCache = new HashMap<>(); + this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); + this.fetchStatusMap = new FetchStatusMap(); + this.executor = new SerialSingleThreadExecutor(); + } + + public IdentityKey getOwnPublicKey() { + return axolotlStore.getIdentityKeyPair().getPublicKey(); + } + + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { + return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust); + } + + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) { + return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust); + } + + public long getNumTrustedKeys(Contact contact) { + return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString()); + } + + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + + private Set<XmppAxolotlSession> findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); + Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); + return ownDeviceSessions; + } + + private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); + return sessions; + } + + private boolean hasAny(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return sessions.hasAny(contactAddress); + } + + public void regenerateKeys() { + axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); + publishBundlesIfNeeded(); + publishOwnDeviceIdIfNeeded(); + } + + public int getOwnDeviceId() { + return axolotlStore.getLocalRegistrationId(); + } + + public Set<Integer> getOwnDeviceIds() { + 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); + XmppAxolotlSession session = sessions.get(address); + if (session != null && session.getFingerprint() != null + && session.getTrust() == from) { + session.setTrust(to); + } + } + } + + public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { + if (jid.toBareJid().equals(account.getJid().toBareJid())) { + if (deviceIds.contains(getOwnDeviceId())) { + deviceIds.remove(getOwnDeviceId()); + } + for (Integer deviceId : deviceIds) { + AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId); + if (sessions.get(ownDeviceAddress) == null) { + buildSessionFromPEP(ownDeviceAddress); + } + } + } + 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.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_UNDECIDED, + XmppAxolotlSession.Trust.UNDECIDED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED, + XmppAxolotlSession.Trust.UNTRUSTED); + this.deviceIds.put(jid, deviceIds); + mXmppConnectionService.keyStatusUpdated(); + publishOwnDeviceIdIfNeeded(); + } + + public void wipeOtherPepDevices() { + Set<Integer> deviceIds = new HashSet<>(); + 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! + } + }); + } + + public void purgeKey(IdentityKey identityKey) { + axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED); + } + + public void publishOwnDeviceIdIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if (deviceIds == null) { + deviceIds = new HashSet<Integer>(); + } + if (!deviceIds.contains(getOwnDeviceId())) { + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing device ID:" + packet.findChild("error")); + } + } + }); + } + + public void publishBundlesIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + boolean flush = false; + if (bundle == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; + } + + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if (flush + || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + } catch (InvalidKeyIdException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + + // Validate PreKeys + Set<PreKeyRecord> preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); + } + + + if (changed) { + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + preKeyRecords, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Published bundle, got: " + packet); + } + }); + } + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + return; + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing Bundle:" + packet.findChild("error")); + } + } + }); + } + + public boolean isContactAxolotlCapable(Contact contact) { + + Jid jid = contact.getJid().toBareJid(); + AxolotlAddress address = new AxolotlAddress(jid.toString(), 0); + return sessions.hasAny(address) || + (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); + } + + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintTrust(fingerprint); + } + + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + axolotlStore.setFingerprintTrust(fingerprint, trust); + } + + private void buildSessionFromPEP(final AxolotlAddress address) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId()); + + try { + IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( + Jid.fromString(address.getName()), address.getDeviceId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + private void finish() { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { + mXmppConnectionService.keyStatusUpdated(); + } + } + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet); + final PreKeyBundle bundle = parser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + finish(); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + //should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + finish(); + return; + } + + final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), + preKey.getPreKeyId(), preKey.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey()); + + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + fetchStatusMap.put(address, FetchStatus.SUCCESS); + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + } + + finish(); + } else { + fetchStatusMap.put(address, FetchStatus.ERROR); + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); + finish(); + return; + } + } + }); + } catch (InvalidJidException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); + } + } + + public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid()); + Jid contactJid = conversation.getContact().getJid().toBareJid(); + Set<AxolotlAddress> addresses = new HashSet<>(); + if (deviceIds.get(contactJid) != null) { + for (Integer foreignId : this.deviceIds.get(contactJid)) { + AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId); + addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + } + } + } + } else { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); + } + 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); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); + addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); + } + } + } + } + + return addresses; + } + + public boolean createSessionsIfNeeded(final Conversation conversation) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation); + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + if (status == null || status == FetchStatus.ERROR) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(address); + newSessions = true; + } else if (status == FetchStatus.PENDING) { + newSessions = true; + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + } + } + + return newSessions; + } + + public boolean hasPendingKeyFetches(Conversation conversation) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0); + return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + || fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING); + + } + + @Nullable + private XmppAxolotlMessage buildHeader(Contact contact) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( + contact.getJid().toBareJid(), getOwnDeviceId()); + + Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact); + Set<XmppAxolotlSession> ownSessions = findOwnSessions(); + if (contactSessions.isEmpty()) { + return null; + } + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements..."); + for (XmppAxolotlSession session : contactSessions) { + 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); + } + + return axolotlMessage; + } + + @Nullable + public XmppAxolotlMessage encrypt(Message message) { + XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact()); + + if (axolotlMessage != null) { + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } + try { + axolotlMessage.encrypt(content); + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + return null; + } + } + + return axolotlMessage; + } + + public void preparePayloadMessage(final Message message, final boolean delay) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = encrypt(message); + if (axolotlMessage == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + //mXmppConnectionService.updateConversationUi(); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay); + } + } + }); + } + + public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = buildHeader(contact); + onMessageCreatedCallback.run(axolotlMessage); + } + }); + } + + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); + if (axolotlMessage != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); + } + return axolotlMessage; + } + + private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + return (identityKey != null) + ? new XmppAxolotlSession(account, axolotlStore, address, + identityKey.getFingerprint().replaceAll("\\s", "")) + : null; + } + + private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { + AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), + message.getSenderDeviceId()); + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); + session = recreateUncachedSession(senderAddress); + if (session == null) { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + } + } + return session; + } + + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + + XmppAxolotlSession session = getReceivingSession(message); + try { + plaintextMessage = message.decrypt(session, getOwnDeviceId()); + Integer preKeyId = session.getPreKeyId(); + if (preKeyId != null) { + publishBundlesIfNeeded(); + session.resetPreKeyId(); + } + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); + } + + if (session.isFresh() && plaintextMessage != null) { + sessions.put(session); + } + + return plaintextMessage; + } + + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null; + + XmppAxolotlSession session = getReceivingSession(message); + keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + + if (session.isFresh() && keyTransportMessage != null) { + sessions.put(session); + } + + return keyTransportMessage; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java new file mode 100644 index 00000000..5796ef30 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class CryptoFailedException extends Exception { + public CryptoFailedException(Exception e){ + super(e); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java new file mode 100644 index 00000000..663b42b5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java @@ -0,0 +1,4 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class NoSessionsCreatedException extends Throwable{ +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java new file mode 100644 index 00000000..3d40a408 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.crypto.axolotl; + +public interface OnMessageCreatedCallback { + void run(XmppAxolotlMessage message); +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java new file mode 100644 index 00000000..190eb88a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java @@ -0,0 +1,421 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.util.Log; +import android.util.LruCache; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.util.List; +import java.util.Set; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; + +public class SQLiteAxolotlStore implements AxolotlStore { + + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String IDENTITIES_TABLENAME = "identities"; + public static final String ACCOUNT = "account"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + 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 OWN = "ownkey"; + + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; + + private static final int NUM_TRUSTS_TO_CACHE = 100; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private IdentityKeyPair identityKeyPair; + private int localRegistrationId; + private int currentPreKeyId = 0; + + private final LruCache<String, XmppAxolotlSession.Trust> trustCache = + new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) { + @Override + protected XmppAxolotlSession.Trust create(String fingerprint) { + return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint); + } + }; + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + } + + private static int generateRegistrationId() { + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); + return KeyHelper.generateRegistrationId(true); + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for (SignedPreKeyRecord record : loadSignedPreKeys()) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + String ownName = account.getJid().toBareJid().toString(); + IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account, + ownName); + + if (ownKey != null) { + return ownKey; + } else { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl key for account " + ownName); + ownKey = generateIdentityKeyPair(); + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); + } + return ownKey; + } + + private int loadRegistrationId() { + return loadRegistrationId(false); + } + + private int loadRegistrationId(boolean regenerate) { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (!regenerate && regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); + reg_id = 0; + } + return reg_id; + } + + public void regenerate() { + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + trustCache.evictAll(); + account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); + identityKeyPair = loadIdentityKeyPair(); + localRegistrationId = loadRegistrationId(true); + currentPreKeyId = 0; + mXmppConnectionService.updateAccountUi(); + } + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + if (identityKeyPair == null) { + identityKeyPair = loadIdentityKeyPair(); + } + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + * <p/> + * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + * <p/> + * Store a remote client's identity key as trusted. + * + * @param name The name of the remote client. + * @param identityKey The remote client's identity key. + */ + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { + mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey); + } + } + + /** + * Verify a remote client's identity key. + * <p/> + * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param name The name of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + return true; + } + + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return (fingerprint == null)? null : trustCache.get(fingerprint); + } + + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust); + trustCache.remove(fingerprint); + } + + public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust); + } + + public long getContactNumTrustedKeys(String bareJid) { + return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, + * or a new SessionRecord if one does not currently exist. + * <p/> + * It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the + * store method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or + * a new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(AxolotlAddress address) { + SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session != null) ? session : new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List<Integer> getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name, 0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + AxolotlAddress address = new AxolotlAddress(name, 0); + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + address); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List<SignedPreKeyRecord> loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java new file mode 100644 index 00000000..cf950d6d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -0,0 +1,249 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.util.Base64; +import android.util.Log; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class XmppAxolotlMessage { + public static final String CONTAINERTAG = "encrypted"; + public static final String HEADER = "header"; + public static final String SOURCEID = "sid"; + public static final String KEYTAG = "key"; + public static final String REMOTEID = "rid"; + public static final String IVTAG = "iv"; + public static final String PAYLOAD = "payload"; + + private static final String KEYTYPE = "AES"; + private static final String CIPHERMODE = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; + + private byte[] innerKey; + private byte[] ciphertext = null; + private byte[] iv = null; + private final Map<Integer, byte[]> keys; + private final Jid from; + private final int sourceDeviceId; + + public static class XmppAxolotlPlaintextMessage { + private final String plaintext; + private final String fingerprint; + + public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) { + this.plaintext = plaintext; + this.fingerprint = fingerprint; + } + + public String getPlaintext() { + return plaintext; + } + + + public String getFingerprint() { + return fingerprint; + } + } + + public static class XmppAxolotlKeyTransportMessage { + private final String fingerprint; + private final byte[] key; + private final byte[] iv; + + public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) { + this.fingerprint = fingerprint; + this.key = key; + this.iv = iv; + } + + public String getFingerprint() { + return fingerprint; + } + + public byte[] getKey() { + return key; + } + + public byte[] getIv() { + return iv; + } + } + + private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException { + this.from = from; + Element header = axolotlMessage.findChild(HEADER); + this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); + List<Element> keyElements = header.getChildren(); + this.keys = new HashMap<>(keyElements.size()); + for (Element keyElement : keyElements) { + switch (keyElement.getName()) { + case KEYTAG: + try { + Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID)); + byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT); + this.keys.put(recipientId, key); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } + break; + case IVTAG: + if (this.iv != null) { + throw new IllegalArgumentException("Duplicate iv entry"); + } + iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT); + break; + default: + Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString()); + break; + } + } + Element payloadElement = axolotlMessage.findChild(PAYLOAD); + if (payloadElement != null) { + ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT); + } + } + + public XmppAxolotlMessage(Jid from, int sourceDeviceId) { + this.from = from; + this.sourceDeviceId = sourceDeviceId; + this.keys = new HashMap<>(); + this.iv = generateIv(); + this.innerKey = generateKey(); + } + + public static XmppAxolotlMessage fromElement(Element element, Jid from) { + return new XmppAxolotlMessage(element, from); + } + + private static byte[] generateKey() { + try { + KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); + generator.init(128); + return generator.generateKey().getEncoded(); + } catch (NoSuchAlgorithmException e) { + Log.e(Config.LOGTAG, e.getMessage()); + return null; + } + } + + private static byte[] generateIv() { + SecureRandom random = new SecureRandom(); + byte[] iv = new byte[16]; + random.nextBytes(iv); + return iv; + } + + public void encrypt(String plaintext) throws CryptoFailedException { + try { + SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + this.innerKey = secretKey.getEncoded(); + this.ciphertext = cipher.doFinal(plaintext.getBytes()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException + | InvalidAlgorithmParameterException e) { + throw new CryptoFailedException(e); + } + } + + public Jid getFrom() { + return this.from; + } + + public int getSenderDeviceId() { + return sourceDeviceId; + } + + public byte[] getCiphertext() { + return ciphertext; + } + + public void addDevice(XmppAxolotlSession session) { + byte[] key = session.processSending(innerKey); + if (key != null) { + keys.put(session.getRemoteAddress().getDeviceId(), key); + } + } + + public byte[] getInnerKey() { + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + public Element toElement() { + Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX); + Element headerElement = encryptionElement.addChild(HEADER); + headerElement.setAttribute(SOURCEID, sourceDeviceId); + 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)); + headerElement.addChild(keyElement); + } + headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT)); + if (ciphertext != null) { + Element payload = encryptionElement.addChild(PAYLOAD); + payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT)); + } + return encryptionElement; + } + + private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) { + byte[] encryptedKey = keys.get(sourceDeviceId); + return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null; + } + + public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) { + byte[] key = unpackKey(session, sourceDeviceId); + return (key != null) + ? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV()) + : null; + } + + public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { + XmppAxolotlPlaintextMessage plaintextMessage = null; + byte[] key = unpackKey(session, sourceDeviceId); + if (key != null) { + try { + Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); + SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint()); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | NoSuchProviderException e) { + throw new CryptoFailedException(e); + } + } + return plaintextMessage; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java new file mode 100644 index 00000000..c4053854 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -0,0 +1,196 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.DuplicateMessageException; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; +import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.UntrustedIdentityException; +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 { + private final SessionCipher cipher; + private final SQLiteAxolotlStore sqLiteAxolotlStore; + private final AxolotlAddress remoteAddress; + private final Account account; + private String fingerprint = null; + 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); + + 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 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 XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { + this(account, store, remoteAddress); + this.fingerprint = fingerprint; + } + + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + this.cipher = new SessionCipher(store, remoteAddress); + this.remoteAddress = remoteAddress; + this.sqLiteAxolotlStore = store; + this.account = account; + } + + public Integer getPreKeyId() { + return preKeyId; + } + + public void resetPreKeyId() { + + preKeyId = null; + } + + public String getFingerprint() { + return fingerprint; + } + + public AxolotlAddress getRemoteAddress() { + return remoteAddress; + } + + public boolean isFresh() { + return fresh; + } + + public void setNotFresh() { + this.fresh = false; + } + + protected void setTrust(Trust trust) { + sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust); + } + + protected Trust getTrust() { + Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint); + return (trust == null) ? Trust.UNDECIDED : trust; + } + + @Nullable + public byte[] processReceiving(byte[] encryptedKey) { + byte[] plaintext = null; + Trust trust = getTrust(); + switch (trust) { + case INACTIVE_TRUSTED: + case UNDECIDED: + case UNTRUSTED: + case TRUSTED: + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", ""); + if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint); + } else { + this.fingerprint = fingerprint; + plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } + } + } 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 && trust == Trust.INACTIVE_TRUSTED) { + setTrust(Trust.TRUSTED); + } + + break; + + case COMPROMISED: + default: + // ignore + break; + } + return plaintext; + } + + @Nullable + public byte[] processSending(@NonNull byte[] outgoingMessage) { + Trust trust = getTrust(); + if (trust == Trust.TRUSTED) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + return ciphertextMessage.serialize(); + } else { + return null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index c95a62df..f47677f6 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -101,6 +101,9 @@ public class ScramSha1 extends SaslMechanism { public String getResponse(final String challenge) throws AuthenticationException { switch (state) { case AUTH_TEXT_SENT: + if (challenge == null) { + throw new AuthenticationException("challenge can not be null"); + } serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); String nonce = ""; diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 90c10199..a8b9252f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OtrService; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -122,6 +123,7 @@ public class Account extends AbstractEntity { protected String avatar; protected boolean online = false; private OtrService mOtrService = null; + private AxolotlService axolotlService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private String otrFingerprint; @@ -255,6 +257,10 @@ public class Account extends AbstractEntity { return keys; } + public String getKey(final String name) { + return this.keys.optString(name, null); + } + public boolean setKey(final String keyName, final String keyValue) { try { this.keys.put(keyName, keyValue); @@ -278,9 +284,13 @@ public class Account extends AbstractEntity { return values; } + public AxolotlService getAxolotlService() { + return axolotlService; + } + public void initAccountServices(final XmppConnectionService context) { - this.mXmppConnectionService = context; this.mOtrService = new OtrService(context, this); + this.axolotlService = new AxolotlService(this, context); } public OtrService getOtrService() { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 81f6568b..2d103faf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -189,20 +189,22 @@ public class Contact implements ListItem, Blockable { } public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(ACCOUNT, accountUuid); - values.put(SYSTEMNAME, systemName); - values.put(SERVERNAME, serverName); - values.put(JID, jid.toString()); - values.put(OPTIONS, subscription); - values.put(SYSTEMACCOUNT, systemAccount); - values.put(PHOTOURI, photoUri); - values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar == null ? null : avatar.getFilename()); - values.put(LAST_PRESENCE, lastseen.presence); - values.put(LAST_TIME, lastseen.time); - values.put(GROUPS, groups.toString()); - return values; + synchronized (this.keys) { + final ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid.toString()); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar == null ? null : avatar.getFilename()); + values.put(LAST_PRESENCE, lastseen.presence); + values.put(LAST_TIME, lastseen.time); + values.put(GROUPS, groups.toString()); + return values; + } } public int getSubscription() { @@ -287,60 +289,65 @@ public class Contact implements ListItem, Blockable { } public ArrayList<String> getOtrFingerprints() { - final ArrayList<String> fingerprints = new ArrayList<String>(); - try { - if (this.keys.has("otr_fingerprints")) { - final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); - 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)); + synchronized (this.keys) { + final ArrayList<String> fingerprints = new ArrayList<String>(); + try { + if (this.keys.has("otr_fingerprints")) { + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); + 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)); + } } } - } - } catch (final JSONException ignored) { + } catch (final JSONException ignored) { + } + return fingerprints; } - return fingerprints; } - public boolean addOtrFingerprint(String print) { - if (getOtrFingerprints().contains(print)) { - return false; - } - try { - JSONArray fingerprints; - if (!this.keys.has("otr_fingerprints")) { - fingerprints = new JSONArray(); - - } else { - fingerprints = this.keys.getJSONArray("otr_fingerprints"); + synchronized (this.keys) { + if (getOtrFingerprints().contains(print)) { + return false; + } + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + return true; + } catch (final JSONException ignored) { + return false; } - fingerprints.put(print); - this.keys.put("otr_fingerprints", fingerprints); - return true; - } catch (final JSONException ignored) { - return false; } } public long getPgpKeyId() { - if (this.keys.has("pgp_keyid")) { - try { - return this.keys.getLong("pgp_keyid"); - } catch (JSONException e) { + synchronized (this.keys) { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { return 0; } - } else { - return 0; } } public void setPgpKeyId(long keyId) { - try { - this.keys.put("pgp_keyid", keyId); - } catch (final JSONException ignored) { - + synchronized (this.keys) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (final JSONException ignored) { + } } } @@ -447,24 +454,26 @@ public class Contact implements ListItem, Blockable { } public boolean deleteOtrFingerprint(String fingerprint) { - boolean success = false; - try { - if (this.keys.has("otr_fingerprints")) { - JSONArray newPrints = new JSONArray(); - JSONArray oldPrints = this.keys - .getJSONArray("otr_fingerprints"); - for (int i = 0; i < oldPrints.length(); ++i) { - if (!oldPrints.getString(i).equals(fingerprint)) { - newPrints.put(oldPrints.getString(i)); - } else { - success = true; + synchronized (this.keys) { + boolean success = false; + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray newPrints = new JSONArray(); + JSONArray oldPrints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < oldPrints.length(); ++i) { + if (!oldPrints.getString(i).equals(fingerprint)) { + newPrints.put(oldPrints.getString(i)); + } else { + success = true; + } } + this.keys.put("otr_fingerprints", newPrints); } - this.keys.put("otr_fingerprints", newPrints); + return success; + } catch (JSONException e) { + return false; } - return success; - } catch (JSONException e) { - return false; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 6ff0e270..1c985e2f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -179,13 +179,13 @@ public class Conversation extends AbstractEntity implements Blockable { } } - public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { synchronized (this.messages) { for (Message message : this.messages) { if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + && (message.getEncryption() == encryptionType)) { onMessageFound.onMessageFound(message); - } + } } } } @@ -519,6 +519,13 @@ public class Conversation extends AbstractEntity implements Blockable { return getContact().getOtrFingerprints().contains(getOtrFingerprint()); } + /** + * short for is Private and Non-anonymous + */ + public boolean isPnNA() { + return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous()); + } + public synchronized MucOptions getMucOptions() { if (this.mucOptions == null) { this.mucOptions = new MucOptions(this); @@ -542,42 +549,51 @@ public class Conversation extends AbstractEntity implements Blockable { return this.nextCounterpart; } - public int getLatestEncryption() { - int latestEncryption = this.getLatestMessage().getEncryption(); - if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) - || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { - return Message.ENCRYPTION_PGP; - } else { - return latestEncryption; + private int getMostRecentlyUsedOutgoingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(i); + if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) { + final int e = m.getEncryption(); + if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { + return Message.ENCRYPTION_PGP; + } else { + return e; + } + } + } } + return Message.ENCRYPTION_NONE; } - public int getNextEncryption(boolean force) { - int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); - if (next == -1) { - int latest = this.getLatestEncryption(); - if (latest == Message.ENCRYPTION_NONE) { - if (force && getMode() == MODE_SINGLE) { - return Message.ENCRYPTION_OTR; - } else if (getContact().getPresences().size() == 1) { - if (getContact().getOtrFingerprints().size() >= 1) { - return Message.ENCRYPTION_OTR; + private int getMostRecentlyUsedIncomingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(i); + if (m.getStatus() == Message.STATUS_RECEIVED) { + final int e = m.getEncryption(); + if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { + return Message.ENCRYPTION_PGP; } else { - return latest; + return e; } - } else { - return latest; } - } else { - return latest; } } - if (next == Message.ENCRYPTION_NONE && force - && getMode() == MODE_SINGLE) { - return Message.ENCRYPTION_OTR; - } else { - return next; + return Message.ENCRYPTION_NONE; + } + + public int getNextEncryption() { + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + int outgoing = this.getMostRecentlyUsedOutgoingEncryption(); + if (outgoing == Message.ENCRYPTION_NONE) { + return this.getMostRecentlyUsedIncomingEncryption(); + } else { + return outgoing; + } } + return next; } public void setNextEncryption(int encryption) { diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index ae9ba1f1..d35a4b01 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -1,28 +1,8 @@ package eu.siacs.conversations.entities; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URLConnection; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.MimeUtils; -import android.util.Log; +import eu.siacs.conversations.utils.MimeUtils; public class DownloadableFile extends File { @@ -30,8 +10,7 @@ public class DownloadableFile extends File { private long expectedSize = 0; private String sha1sum; - private Key aeskey; - private String mime; + private byte[] aeskey; private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; @@ -45,15 +24,7 @@ public class DownloadableFile extends File { } public long getExpectedSize() { - if (this.aeskey != null) { - if (this.expectedSize == 0) { - return 0; - } else { - return (this.expectedSize / 16 + 1) * 16; - } - } else { - return this.expectedSize; - } + return this.expectedSize; } public String getMimeType() { @@ -79,91 +50,38 @@ public class DownloadableFile extends File { this.sha1sum = sum; } - public void setKey(byte[] key) { - if (key.length == 48) { + public void setKeyAndIv(byte[] keyIvCombo) { + if (keyIvCombo.length == 48) { byte[] secretKey = new byte[32]; byte[] iv = new byte[16]; - System.arraycopy(key, 0, iv, 0, 16); - System.arraycopy(key, 16, secretKey, 0, 32); - this.aeskey = new SecretKeySpec(secretKey, "AES"); + System.arraycopy(keyIvCombo, 0, iv, 0, 16); + System.arraycopy(keyIvCombo, 16, secretKey, 0, 32); + this.aeskey = secretKey; this.iv = iv; - } else if (key.length >= 32) { + } else if (keyIvCombo.length >= 32) { byte[] secretKey = new byte[32]; - System.arraycopy(key, 0, secretKey, 0, 32); - this.aeskey = new SecretKeySpec(secretKey, "AES"); - } else if (key.length >= 16) { + System.arraycopy(keyIvCombo, 0, secretKey, 0, 32); + this.aeskey = secretKey; + } else if (keyIvCombo.length >= 16) { byte[] secretKey = new byte[16]; - System.arraycopy(key, 0, secretKey, 0, 16); - this.aeskey = new SecretKeySpec(secretKey, "AES"); + System.arraycopy(keyIvCombo, 0, secretKey, 0, 16); + this.aeskey = secretKey; } } - public Key getKey() { - return this.aeskey; + public void setKey(byte[] key) { + this.aeskey = key; } - public InputStream createInputStream() { - if (this.getKey() == null) { - try { - return new FileInputStream(this); - } catch (FileNotFoundException e) { - return null; - } - } else { - try { - IvParameterSpec ips = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips); - Log.d(Config.LOGTAG, "opening encrypted input stream"); - return new CipherInputStream(new FileInputStream(this), cipher); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } catch (FileNotFoundException e) { - return null; - } - } + public void setIv(byte[] iv) { + this.iv = iv; } - public OutputStream createOutputStream() { - if (this.getKey() == null) { - try { - return new FileOutputStream(this); - } catch (FileNotFoundException e) { - return null; - } - } else { - try { - IvParameterSpec ips = new IvParameterSpec(this.iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips); - Log.d(Config.LOGTAG, "opening encrypted output stream"); - return new CipherOutputStream(new FileOutputStream(this), - cipher); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } catch (FileNotFoundException e) { - return null; - } - } + public byte[] getKey() { + return this.aeskey; + } + + public byte[] getIv() { + return this.iv; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index d04dff87..6a878e90 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -8,6 +8,7 @@ import java.net.URL; import java.util.Arrays; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; @@ -34,6 +35,7 @@ public class Message extends AbstractEntity { public static final int ENCRYPTION_OTR = 2; public static final int ENCRYPTION_DECRYPTED = 3; public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + public static final int ENCRYPTION_AXOLOTL = 5; public static final int TYPE_TEXT = 0; public static final int TYPE_IMAGE = 1; @@ -49,9 +51,11 @@ public class Message extends AbstractEntity { public static final String ENCRYPTION = "encryption"; public static final String STATUS = "status"; public static final String TYPE = "type"; + public static final String CARBON = "carbon"; public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String FINGERPRINT = "axolotl_fingerprint"; public static final String ME_COMMAND = "/me "; @@ -65,6 +69,7 @@ public class Message extends AbstractEntity { protected int encryption; protected int status; protected int type; + protected boolean carbon = false; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -73,6 +78,7 @@ public class Message extends AbstractEntity { protected Transferable transferable = null; private Message mNextMessage = null; private Message mPreviousMessage = null; + private String axolotlFingerprint = null; private Message() { @@ -81,8 +87,11 @@ public class Message extends AbstractEntity { public Message(Conversation conversation, String body, int encryption) { this(conversation, body, encryption, STATUS_UNSEND); } - public Message(Conversation conversation, String body, int encryption, int status) { + this(conversation, body, encryption, status, false); + } + + public Message(Conversation conversation, String body, int encryption, int status, boolean carbon) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().toBareJid(), @@ -92,6 +101,8 @@ public class Message extends AbstractEntity { encryption, status, TYPE_TEXT, + false, + null, null, null, null); @@ -100,8 +111,9 @@ public class Message extends AbstractEntity { private Message(final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final String remoteMsgId, - final String relativeFilePath, final String serverMsgId) { + final int encryption, final int status, final int type, final boolean carbon, + final String remoteMsgId, final String relativeFilePath, + final String serverMsgId, final String fingerprint) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -111,9 +123,11 @@ public class Message extends AbstractEntity { this.encryption = encryption; this.status = status; this.type = type; + this.carbon = carbon; this.remoteMsgId = remoteMsgId; this.relativeFilePath = relativeFilePath; this.serverMsgId = serverMsgId; + this.axolotlFingerprint = fingerprint; } public static Message fromCursor(Cursor cursor) { @@ -148,9 +162,11 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getInt(cursor.getColumnIndex(CARBON))>0, cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndex(FINGERPRINT))); } public static Message createStatusMessage(Conversation conversation, String body) { @@ -181,9 +197,11 @@ public class Message extends AbstractEntity { values.put(ENCRYPTION, encryption); values.put(STATUS, status); values.put(TYPE, type); + values.put(CARBON, carbon ? 1 : 0); values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); + values.put(FINGERPRINT, axolotlFingerprint); return values; } @@ -304,6 +322,14 @@ public class Message extends AbstractEntity { this.type = type; } + public boolean isCarbon() { + return carbon; + } + + public void setCarbon(boolean carbon) { + this.carbon = carbon; + } + public void setTrueCounterpart(Jid trueCounterpart) { this.trueCounterpart = trueCounterpart; } @@ -389,7 +415,8 @@ public class Message extends AbstractEntity { message.treatAsDownloadable() == Decision.NEVER && this.treatAsDownloadable() == Decision.NEVER && !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) + !this.getBody().startsWith(ME_COMMAND) && + this.isTrusted() == message.isTrusted() ); } @@ -405,11 +432,14 @@ public class Message extends AbstractEntity { } public String getMergedBody() { - final Message next = this.next(); - if (this.mergeable(next)) { - return getBody() + MERGE_SEPARATOR + next.getMergedBody(); + StringBuilder body = new StringBuilder(this.body.trim()); + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + body.append(MERGE_SEPARATOR); + body.append(current.getBody()); } - return getBody(); + return body.toString(); } public boolean hasMeCommand() { @@ -417,20 +447,23 @@ public class Message extends AbstractEntity { } public int getMergedStatus() { - final Message next = this.next(); - if (this.mergeable(next)) { - return next.getStatus(); + int status = this.status; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + status = current.status; } - return getStatus(); + return status; } public long getMergedTimeSent() { - Message next = this.next(); - if (this.mergeable(next)) { - return next.getMergedTimeSent(); - } else { - return getTimeSent(); + long time = this.timeSent; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + time = current.timeSent; } + return time; } public boolean wasMergedIntoPrevious() { @@ -673,4 +706,48 @@ public class Message extends AbstractEntity { public int width = 0; public int height = 0; } + + public void setAxolotlFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; + } + + public String getAxolotlFingerprint() { + return axolotlFingerprint; + } + + public boolean isTrusted() { + return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint) + == XmppAxolotlSession.Trust.TRUSTED; + } + + private int getPreviousEncryption() { + for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){ + if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + continue; + } + return iterator.getEncryption(); + } + return ENCRYPTION_NONE; + } + + private int getNextEncryption() { + for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){ + if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + continue; + } + return iterator.getEncryption(); + } + return conversation.getNextEncryption(); + } + + public boolean isValidInSession() { + int pastEncryption = this.getPreviousEncryption(); + int futureEncryption = this.getNextEncryption(); + + boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; + + return inUnencryptedSession || this.getEncryption() == pastEncryption; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index d867a370..52a862ef 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.entities; +import android.annotation.SuppressLint; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -11,8 +13,6 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import android.annotation.SuppressLint; - @SuppressLint("DefaultLocale") public class MucOptions { @@ -264,6 +264,15 @@ public class MucOptions { users.add(user); } + public boolean isUserInRoom(String name) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(name)) { + return true; + } + } + return false; + } + public void processPacket(PresencePacket packet, PgpEngine pgp) { final Jid from = packet.getFrom(); if (!from.isBareJid()) { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 16984910..3a256219 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -13,6 +13,7 @@ import java.util.Locale; import java.util.TimeZone; import de.tzur.conversations.Settings; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PhoneHelper; @@ -29,7 +30,8 @@ public abstract class AbstractGenerator { "urn:xmpp:avatar:metadata+notify", "urn:xmpp:ping", "jabber:iq:version", - "http://jabber.org/protocol/chatstates"}; + "http://jabber.org/protocol/chatstates", + AxolotlService.PEP_DEVICE_LIST+"+notify"}; private final String[] MESSAGE_CONFIRMATION_FEATURES = { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 47915e3f..898d218e 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -1,15 +1,23 @@ package eu.siacs.conversations.generator; +import android.util.Base64; + +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + import java.util.ArrayList; import java.util.List; +import java.util.Set; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.forms.Data; @@ -115,6 +123,56 @@ public class IqGenerator extends AbstractGenerator { return packet; } + public IqPacket retrieveDeviceIds(final Jid to) { + final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket publishDeviceIds(final Set<Integer> ids) { + final Element item = new Element("item"); + final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); + for(Integer id:ids) { + final Element device = new Element("device"); + device.setAttribute("id", id); + list.addChild(device); + } + return publish(AxolotlService.PEP_DEVICE_LIST, item); + } + + public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, + final Set<PreKeyRecord> preKeyRecords, final int deviceId) { + final Element item = new Element("item"); + final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); + final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); + signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); + ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); + signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT)); + final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); + signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT)); + final Element identityKeyElement = bundle.addChild("identityKey"); + identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + + final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:preKeyRecords) { + final Element prekey = prekeys.addChild("preKeyPublic"); + prekey.setAttribute("preKeyId", preKeyRecord.getId()); + prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); + } + + return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item); + } + public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element query = packet.query("urn:xmpp:mam:0"); @@ -196,12 +254,15 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file) { + public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(host); Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD); request.addChild("filename").setContent(file.getName()); request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); + if (mime != null) { + request.addChild("content-type", mime); + } return packet; } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index e698c151..a06a0ddd 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -1,12 +1,14 @@ package eu.siacs.conversations.generator; +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; @@ -21,7 +23,7 @@ public class MessageGenerator extends AbstractGenerator { super(service); } - private MessagePacket preparePacket(Message message, boolean addDelay) { + private MessagePacket preparePacket(Message message) { Conversation conversation = message.getConversation(); Account account = conversation.getAccount(); MessagePacket packet = new MessagePacket(); @@ -44,13 +46,10 @@ public class MessageGenerator extends AbstractGenerator { } packet.setFrom(account.getJid()); packet.setId(message.getUuid()); - if (addDelay) { - addDelay(packet, message.getTimeSent()); - } return packet; } - private void addDelay(MessagePacket packet, long timestamp) { + public void addDelay(MessagePacket packet, long timestamp) { final SimpleDateFormat mDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -59,16 +58,21 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateOtrChat(Message message) { - return generateOtrChat(message, false); + public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { + MessagePacket packet = preparePacket(message); + if (axolotlMessage == null) { + return null; + } + packet.setAxolotlMessage(axolotlMessage.toElement()); + return packet; } - public MessagePacket generateOtrChat(Message message, boolean addDelay) { + public MessagePacket generateOtrChat(Message message) { Session otrSession = message.getConversation().getOtrSession(); if (otrSession == null) { return null; } - MessagePacket packet = preparePacket(message, addDelay); + MessagePacket packet = preparePacket(message); packet.addChild("private", "urn:xmpp:carbons:2"); packet.addChild("no-copy", "urn:xmpp:hints"); packet.addChild("no-permanent-store", "urn:xmpp:hints"); @@ -88,11 +92,7 @@ public class MessageGenerator extends AbstractGenerator { } public MessagePacket generateChat(Message message) { - return generateChat(message, false); - } - - public MessagePacket generateChat(Message message, boolean addDelay) { - MessagePacket packet = preparePacket(message, addDelay); + MessagePacket packet = preparePacket(message); if (message.hasFileOnRemoteHost()) { packet.setBody(message.getFileParams().url.toString()); } else { @@ -102,11 +102,7 @@ public class MessageGenerator extends AbstractGenerator { } public MessagePacket generatePgpChat(Message message) { - return generatePgpChat(message, false); - } - - public MessagePacket generatePgpChat(Message message, boolean addDelay) { - MessagePacket packet = preparePacket(message, addDelay); + MessagePacket packet = preparePacket(message); packet.setBody("This is an XEP-0027 encrypted message"); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody()); @@ -119,25 +115,26 @@ public class MessageGenerator extends AbstractGenerator { public MessagePacket generateChatState(Conversation conversation) { final Account account = conversation.getAccount(); MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); packet.setTo(conversation.getJid().toBareJid()); packet.setFrom(account.getJid()); packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); + packet.addChild("no-store", "urn:xmpp:hints"); + packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store* return packet; } public MessagePacket confirm(final Account account, final Jid to, final String id) { MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); + packet.setType(MessagePacket.TYPE_CHAT); packet.setTo(to); packet.setFrom(account.getJid()); - Element received = packet.addChild("displayed", - "urn:xmpp:chat-markers:0"); + Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0"); received.setAttribute("id", id); return packet; } - public MessagePacket conferenceSubject(Conversation conversation, - String subject) { + public MessagePacket conferenceSubject(Conversation conversation,String subject) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_GROUPCHAT); packet.setTo(conversation.getJid().toBareJid()); @@ -171,10 +168,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket received(Account account, - MessagePacket originalMessage, String namespace) { + public MessagePacket received(Account account, MessagePacket originalMessage, String namespace, int type) { MessagePacket receivedPacket = new MessagePacket(); - receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setType(type); receivedPacket.setTo(originalMessage.getFrom()); receivedPacket.setFrom(account.getJid()); Element received = receivedPacket.addChild("received", namespace); diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 58a6d1e3..90fbadfe 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -38,9 +38,9 @@ public class HttpConnectionManager extends AbstractConnectionManager { return connection; } - public HttpUploadConnection createNewUploadConnection(Message message) { + public HttpUploadConnection createNewUploadConnection(Message message, boolean delay) { HttpUploadConnection connection = new HttpUploadConnection(this); - connection.init(message); + connection.init(message,delay); this.uploadConnections.add(connection); return connection; } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 45f1e965..356f9417 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -2,11 +2,12 @@ package eu.siacs.conversations.http; import android.content.Intent; import android.net.Uri; -import android.os.SystemClock; +import android.os.PowerManager; import android.util.Log; import java.io.BufferedInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -18,9 +19,11 @@ import javax.net.ssl.SSLHandshakeException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; @@ -35,7 +38,6 @@ public class HttpDownloadConnection implements Transferable { private int mStatus = Transferable.STATUS_UNKNOWN; private boolean acceptedAutomatically = false; private int mProgress = 0; - private long mLastGuiRefresh = 0; public HttpDownloadConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; @@ -70,7 +72,8 @@ public class HttpDownloadConnection implements Transferable { String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null; if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) { this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; @@ -83,10 +86,11 @@ public class HttpDownloadConnection implements Transferable { this.file = mXmppConnectionService.getFileBackend().getFile(message, false); String reference = mUrl.getRef(); if (reference != null && reference.length() == 96) { - this.file.setKey(CryptoHelper.hexToBytes(reference)); + this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); } - if (this.message.getEncryption() == Message.ENCRYPTION_OTR + if ((this.message.getEncryption() == Message.ENCRYPTION_OTR + || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } @@ -123,6 +127,17 @@ public class HttpDownloadConnection implements Transferable { mXmppConnectionService.updateConversationUi(); } + private void showToastForException(Exception e) { + e.printStackTrace(); + 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 { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); + } + } + private class FileSizeChecker implements Runnable { private boolean interactive = false; @@ -144,7 +159,7 @@ public class HttpDownloadConnection implements Transferable { } catch (IOException e) { Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); if (interactive) { - mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host); + showToastForException(e); } cancel(); return; @@ -186,6 +201,8 @@ public class HttpDownloadConnection implements Transferable { private boolean interactive = false; + private OutputStream os; + public FileDownloader(boolean interactive) { this.interactive = interactive; } @@ -200,36 +217,44 @@ public class HttpDownloadConnection implements Transferable { } catch (SSLHandshakeException e) { changeStatus(STATUS_OFFER); } catch (IOException e) { - mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host); + if (interactive) { + showToastForException(e); + } cancel(); } } - private void download() throws SSLHandshakeException, IOException { - HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.connect(); - BufferedInputStream is = new BufferedInputStream(connection.getInputStream()); - file.getParentFile().mkdirs(); - file.createNewFile(); - OutputStream os = file.createOutputStream(); - if (os == null) { - throw new IOException(); - } - long transmitted = 0; - long expected = file.getExpectedSize(); - int count = -1; - byte[] buffer = new byte[1024]; - while ((count = is.read(buffer)) != -1) { - transmitted += count; - os.write(buffer, 0, count); - updateProgress((int) ((((double) transmitted) / expected) * 100)); + private void download() throws IOException { + InputStream is = null; + PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid()); + try { + wakeLock.acquire(); + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + is = new BufferedInputStream(connection.getInputStream()); + file.getParentFile().mkdirs(); + file.createNewFile(); + os = AbstractConnectionManager.createOutputStream(file, true); + long transmitted = 0; + long expected = file.getExpectedSize(); + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + transmitted += count; + os.write(buffer, 0, count); + updateProgress((int) ((((double) transmitted) / expected) * 100)); + } + os.flush(); + } catch (IOException e) { + throw e; + } finally { + FileBackend.close(os); + FileBackend.close(is); + wakeLock.release(); } - os.flush(); - os.close(); - is.close(); } private void updateImageBounds() { @@ -242,10 +267,7 @@ public class HttpDownloadConnection implements Transferable { public void updateProgress(int i) { this.mProgress = i; - if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { - this.mLastGuiRefresh = SystemClock.elapsedRealtime(); - mXmppConnectionService.updateConversationUi(); - } + mXmppConnectionService.updateConversationUi(); } @Override diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index a3ab8dab..2e545842 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,8 +1,13 @@ package eu.siacs.conversations.http; import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.os.PowerManager; import android.util.Log; +import android.util.Pair; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -14,10 +19,11 @@ import javax.net.ssl.HttpsURLConnection; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.utils.CryptoHelper; @@ -33,16 +39,19 @@ public class HttpUploadConnection implements Transferable { private XmppConnectionService mXmppConnectionService; private boolean canceled = false; + private boolean delayed = false; private Account account; private DownloadableFile file; private Message message; + private String mime; private URL mGetUrl; private URL mPutUrl; private byte[] key = null; private long transmitted = 0; - private long expected = 1; + + private InputStream mFileInputStream; public HttpUploadConnection(HttpConnectionManager httpConnectionManager) { this.mHttpConnectionManager = httpConnectionManager; @@ -66,7 +75,7 @@ public class HttpUploadConnection implements Transferable { @Override public int getProgress() { - return (int) ((((double) transmitted) / expected) * 100); + return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); } @Override @@ -77,25 +86,36 @@ public class HttpUploadConnection implements Transferable { private void fail() { mHttpConnectionManager.finishUploadConnection(this); message.setTransferable(null); - mXmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED); + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + FileBackend.close(mFileInputStream); } - public void init(Message message) { + public void init(Message message, boolean delay) { this.message = message; message.setTransferable(this); - mXmppConnectionService.markMessage(message,Message.STATUS_UNSEND); + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); this.account = message.getConversation().getAccount(); this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - this.file.setExpectedSize(this.file.getSize()); - - if (Config.ENCRYPT_ON_HTTP_UPLOADED) { + this.mime = this.file.getMimeType(); + this.delayed = delay; + if (Config.ENCRYPT_ON_HTTP_UPLOADED + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL + || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[48]; mXmppConnectionService.getRNG().nextBytes(this.key); - this.file.setKey(this.key); + this.file.setKeyAndIv(this.key); } - + Pair<InputStream,Integer> pair; + try { + pair = AbstractConnectionManager.createInputStream(file, true); + } catch (FileNotFoundException e) { + fail(); + return; + } + this.file.setExpectedSize(pair.second); + this.mFileInputStream = pair.first; Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD); - IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file); + IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime); mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -130,9 +150,10 @@ public class HttpUploadConnection implements Transferable { private void upload() { OutputStream os = null; - InputStream is = null; HttpURLConnection connection = null; + PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid()); try { + wakeLock.acquire(); Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); connection = (HttpURLConnection) mPutUrl.openConnection(); if (connection instanceof HttpsURLConnection) { @@ -140,37 +161,38 @@ public class HttpUploadConnection implements Transferable { } connection.setRequestMethod("PUT"); connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); + connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime); connection.setDoOutput(true); connection.connect(); os = connection.getOutputStream(); - is = file.createInputStream(); transmitted = 0; - expected = file.getExpectedSize(); int count = -1; byte[] buffer = new byte[4096]; - while (((count = is.read(buffer)) != -1) && !canceled) { + while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) { transmitted += count; os.write(buffer, 0, count); mXmppConnectionService.updateConversationUi(); } os.flush(); os.close(); - is.close(); + mFileInputStream.close(); int code = connection.getResponseCode(); if (code == 200 || code == 201) { Log.d(Config.LOGTAG, "finished uploading file"); - Message.FileParams params = message.getFileParams(); if (key != null) { mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); } mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl); + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); message.setTransferable(null); message.setCounterpart(message.getConversation().getJid().toBareJid()); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { mXmppConnectionService.getPgpEngine().encrypt(message, new UiCallback<Message>() { @Override public void success(Message message) { - mXmppConnectionService.resendMessage(message); + mXmppConnectionService.resendMessage(message,delayed); } @Override @@ -184,20 +206,22 @@ public class HttpUploadConnection implements Transferable { } }); } else { - mXmppConnectionService.resendMessage(message); + mXmppConnectionService.resendMessage(message, delayed); } } else { fail(); } } catch (IOException e) { - Log.d(Config.LOGTAG, e.getMessage()); + e.printStackTrace(); + Log.d(Config.LOGTAG,"http upload failed "+e.getMessage()); fail(); } finally { - FileBackend.close(is); + FileBackend.close(mFileInputStream); FileBackend.close(os); if (connection != null) { connection.disconnect(); } + wakeLock.release(); } } } diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 11ddcc13..3f11f756 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -76,11 +76,9 @@ public abstract class AbstractParser { return dateFormat.parse(timestamp); } - protected void updateLastseen(final Element packet, final Account account, - final boolean presenceOverwrite) { - final Jid from = packet.getAttributeAsJid("from"); - updateLastseen(packet, account, from, presenceOverwrite); - } + protected void updateLastseen(final Element packet, final Account account, final boolean presenceOverwrite) { + updateLastseen(packet, account, packet.getAttributeAsJid("from"), presenceOverwrite); + } protected void updateLastseen(final Element packet, final Account account, final Jid from, final boolean presenceOverwrite) { final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 6039d395..cef5b03d 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,11 +1,25 @@ package eu.siacs.conversations.parser; +import android.support.annotation.NonNull; +import android.util.Base64; import android.util.Log; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyBundle; + import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; 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.services.XmppConnectionService; @@ -71,9 +85,160 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return super.avatarData(items); } + public Element getItem(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + final Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return items.findChild("item"); + } + + @NonNull + public Set<Integer> deviceIds(final Element item) { + Set<Integer> deviceIds = new HashSet<>(); + if (item != null) { + final Element list = item.findChild("list"); + if (list != null) { + for (Element device : list.getChildren()) { + if (!device.getName().equals("device")) { + continue; + } + try { + Integer id = Integer.valueOf(device.getAttribute("id")); + deviceIds.add(id); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid <device> node in PEP:" + device.toString() + + ", skipping..."); + continue; + } + } + } + } + return deviceIds; + } + + public Integer signedPreKeyId(final Element bundle) { + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if(signedPreKeyPublic == null) { + return null; + } + return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); + } + + public ECPublicKey signedPreKeyPublic(final Element bundle) { + ECPublicKey publicKey = null; + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if(signedPreKeyPublic == null) { + return null; + } + try { + publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage()); + } + return publicKey; + } + + public byte[] signedPreKeySignature(final Element bundle) { + final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature"); + if(signedPreKeySignature == null) { + return null; + } + return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT); + } + + public IdentityKey identityKey(final Element bundle) { + IdentityKey identityKey = null; + final Element identityKeyElement = bundle.findChild("identityKey"); + if(identityKeyElement == null) { + return null; + } + try { + identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); + } + return identityKey; + } + + public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) { + Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>(); + Element item = getItem(packet); + if (item == null) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet); + return null; + } + final Element bundleElement = item.findChild("bundle"); + if(bundleElement == null) { + return null; + } + final Element prekeysElement = bundleElement.findChild("prekeys"); + if(prekeysElement == null) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet); + return null; + } + for(Element preKeyPublicElement : prekeysElement.getChildren()) { + if(!preKeyPublicElement.getName().equals("preKeyPublic")){ + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + continue; + } + Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); + try { + ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); + preKeyRecords.put(preKeyId, preKeyPublic); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); + continue; + } + } + return preKeyRecords; + } + + public PreKeyBundle bundle(final IqPacket bundle) { + Element bundleItem = getItem(bundle); + if(bundleItem == null) { + return null; + } + final Element bundleElement = bundleItem.findChild("bundle"); + if(bundleElement == null) { + return null; + } + ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); + Integer signedPreKeyId = signedPreKeyId(bundleElement); + byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); + IdentityKey identityKey = identityKey(bundleElement); + if(signedPreKeyPublic == null || identityKey == null) { + return null; + } + + return new PreKeyBundle(0, 0, 0, null, + signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); + } + + public List<PreKeyBundle> preKeys(final IqPacket preKeys) { + List<PreKeyBundle> bundles = new ArrayList<>(); + Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys); + if ( preKeyPublics != null) { + for (Integer preKeyId : preKeyPublics.keySet()) { + ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); + bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, + 0, null, null, null)); + } + } + + return bundles; + } + @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) { + if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; + } else if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) { final Element query = packet.findChild("query"); // If this is in response to a query for the whole roster: if (packet.getType() == IqPacket.TYPE.RESULT) { @@ -143,15 +308,13 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } else { - if ((packet.getType() == IqPacket.TYPE.GET) - || (packet.getType() == IqPacket.TYPE.SET)) { + if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); - error.addChild("feature-not-implemented", - "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas"); account.getXmppConnection().sendIqPacket(response, null); - } + } } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 81fd89cd..e2f13bd3 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -6,8 +6,12 @@ import android.util.Pair; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import java.util.Set; + import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -70,11 +74,9 @@ public class MessageParser extends AbstractParser implements body = otrSession.transformReceiving(body); SessionStatus status = otrSession.getSessionStatus(); if (body == null && status == SessionStatus.ENCRYPTED) { - conversation.setNextEncryption(Message.ENCRYPTION_OTR); mXmppConnectionService.onOtrSessionEstablished(conversation); return null; } else if (body == null && status == SessionStatus.FINISHED) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); conversation.resetOtrSession(); mXmppConnectionService.updateConversationUi(); return null; @@ -95,6 +97,20 @@ public class MessageParser extends AbstractParser implements } } + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) { + Message finishedMessage = null; + AxolotlService service = conversation.getAccount().getAxolotlService(); + XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid()); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage); + if(plaintextMessage != null) { + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); + finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); + } + + return finishedMessage; + } + private class Invite { Jid jid; String password; @@ -102,6 +118,8 @@ public class MessageParser extends AbstractParser implements this.jid = jid; this.password = password; } + return false; + } public boolean execute(Account account) { if (jid != null) { @@ -171,6 +189,13 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } + } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing..."); + Element item = items.findChild("item"); + Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + AxolotlService axolotlService = account.getAxolotlService(); + axolotlService.registerDevices(from, deviceIds); + mXmppConnectionService.updateAccountUi(); } } @@ -178,6 +203,13 @@ public class MessageParser extends AbstractParser implements 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(), @@ -199,6 +231,7 @@ public class MessageParser extends AbstractParser implements final MessagePacket packet; Long timestamp = null; final boolean isForwarded; + boolean isCarbon = false; String serverMsgId = null; final Element fin = original.findChild("fin", "urn:xmpp:mam:0"); if (fin != null) { @@ -229,7 +262,8 @@ public class MessageParser extends AbstractParser implements return; } timestamp = f != null ? f.second : null; - isForwarded = f != null; + isCarbon = f != null; + isForwarded = isCarbon; } else { packet = original; isForwarded = false; @@ -239,8 +273,9 @@ public class MessageParser extends AbstractParser implements timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); } final String body = packet.getBody(); - final String encrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user"); + final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); + final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -268,11 +303,11 @@ public class MessageParser extends AbstractParser implements return; } - if (extractChatState(mXmppConnectionService.find(account,from), packet)) { + if (extractChatState(mXmppConnectionService.find(account, from), packet)) { mXmppConnectionService.updateConversationUi(); } - if ((body != null || encrypted != null) && !isMucStatusMessage) { + if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) { Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat); if (isTypeGroupChat) { if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { @@ -301,14 +336,20 @@ public class MessageParser extends AbstractParser implements } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } - } else if (encrypted != null) { - message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status); + } else if (pgpEncrypted != null) { + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); + } else if (axolotlEncrypted != null) { + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status); + if (message == null) { + return; + } } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } message.setCounterpart(counterpart); message.setRemoteMsgId(remoteMsgId); message.setServerMsgId(serverMsgId); + message.setCarbon(isCarbon); message.setTime(timestamp); message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); if (conversation.getMode() == Conversation.MODE_MULTI) { @@ -339,15 +380,19 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateConversationUi(); } - if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded) { + if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { - MessagePacket receipt = mXmppConnectionService - .getMessageGenerator().received(account, packet, "urn:xmpp:chat-markers:0"); + MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, + packet, + "urn:xmpp:chat-markers:0", + MessagePacket.TYPE_CHAT); mXmppConnectionService.sendMessagePacket(account, receipt); } if (packet.hasChild("request", "urn:xmpp:receipts")) { - MessagePacket receipt = mXmppConnectionService - .getMessageGenerator().received(account, packet, "urn:xmpp:receipts"); + MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, + packet, + "urn:xmpp:receipts", + packet.getType()); mXmppConnectionService.sendMessagePacket(account, receipt); } } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index d11b02fa..9fe47512 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -1,10 +1,34 @@ package eu.siacs.conversations.persistance; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Base64; +import android.util.Log; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + +import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +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; @@ -13,19 +37,12 @@ import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteCantOpenDatabaseException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 14; + private static final int DATABASE_VERSION = 16; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -33,12 +50,66 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " - + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " + Contact.JID + ") ON CONFLICT REPLACE);"; + private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.PREKEY_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.ID + + ") ON CONFLICT REPLACE" + +");"; + + private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.ID + + ") ON CONFLICT REPLACE"+ + ");"; + + private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.SESSION_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.NAME + " TEXT, " + + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.NAME + ", " + + SQLiteAxolotlStore.DEVICE_ID + + ") ON CONFLICT REPLACE" + +");"; + + private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.NAME + " TEXT, " + + SQLiteAxolotlStore.OWN + " INTEGER, " + + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + + SQLiteAxolotlStore.TRUSTED + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.NAME + ", " + + SQLiteAxolotlStore.FINGERPRINT + + ") ON CONFLICT IGNORE" + +");"; + private DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @@ -69,12 +140,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + Message.RELATIVE_FILE_PATH + " TEXT, " + Message.SERVER_MSG_ID + " TEXT, " + + Message.FINGERPRINT + " TEXT, " + + Message.CARBON + " INTEGER, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID + ") ON DELETE CASCADE);"); db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL(CREATE_IDENTITIES_STATEMENT); } @Override @@ -109,12 +186,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + Conversation.ATTRIBUTES + " TEXT"); } - if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); - } + if (oldVersion < 9 && newVersion >= 9) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_TIME + " NUMBER"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_PRESENCE + " TEXT"); + } if (oldVersion < 10 && newVersion >= 10) { db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.RELATIVE_FILE_PATH + " TEXT"); @@ -215,6 +292,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { } cursor.close(); } + if (oldVersion < 15 && newVersion >= 15) { + recreateAxolotlDb(db); + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.FINGERPRINT + " TEXT"); + } + if (oldVersion < 16 && newVersion >= 16) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.CARBON + " INTEGER"); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -311,7 +397,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { }; Cursor cursor = db.query(Conversation.TABLENAME, null, Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null); + + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null); if (cursor.getCount() == 0) return null; cursor.moveToFirst(); @@ -481,4 +567,405 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.close(); return list; } + + private Cursor getCursorForSession(Account account, AxolotlAddress contact) { + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = null; + String[] selectionArgs = {account.getUuid(), + contact.getName(), + Integer.toString(contact.getDeviceId())}; + Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + " = ? ", + selectionArgs, + null, null, null); + + return cursor; + } + + public SessionRecord loadSession(Account account, AxolotlAddress contact) { + SessionRecord session = null; + Cursor cursor = getCursorForSession(account, contact); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + } catch (IOException e) { + cursor.close(); + throw new AssertionError(e); + } + } + cursor.close(); + return session; + } + + public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) { + List<Integer> devices = new ArrayList<>(); + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.DEVICE_ID}; + String[] selectionArgs = {account.getUuid(), + contact.getName()}; + Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ?", + selectionArgs, + null, null, null); + + while(cursor.moveToNext()) { + devices.add(cursor.getInt( + cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); + } + + cursor.close(); + return devices; + } + + public boolean containsSession(Account account, AxolotlAddress contact) { + Cursor cursor = getCursorForSession(account, contact); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.NAME, contact.getName()); + values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + } + + public void deleteSession(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), + contact.getName(), + Integer.toString(contact.getDeviceId())}; + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + " = ? ", + args); + } + + public void deleteAllSessions(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), contact.getName()}; + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.NAME + " = ?", + args); + } + + private Cursor getCursorForPreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; + Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public PreKeyRecord loadPreKey(Account account, int preKeyId) { + PreKeyRecord record = null; + Cursor cursor = getCursorForPreKey(account, preKeyId); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + } catch (IOException e ) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public boolean containsPreKey(Account account, int preKeyId) { + Cursor cursor = getCursorForPreKey(account, preKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storePreKey(Account account, PreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ID, record.getId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); + } + + public void deletePreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(preKeyId)}; + db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", + args); + } + + private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; + Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { + SignedPreKeyRecord record = null; + Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + } catch (IOException e ) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) { + List<SignedPreKeyRecord> prekeys = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid()}; + Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=?", + selectionArgs, + null, null, null); + + while(cursor.moveToNext()) { + try { + prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); + } catch (IOException ignored) { + } + } + cursor.close(); + return prekeys; + } + + public boolean containsSignedPreKey(Account account, int signedPreKeyId) { + Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ID, record.getId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); + } + + public void deleteSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; + db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", + args); + } + + private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { + return getIdentityKeyCursor(account, name, own, null); + } + + private Cursor getIdentityKeyCursor(Account account, String fingerprint) { + return getIdentityKeyCursor(account, null, null, fingerprint); + } + + private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) { + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.TRUSTED, + SQLiteAxolotlStore.KEY}; + ArrayList<String> selectionArgs = new ArrayList<>(4); + selectionArgs.add(account.getUuid()); + String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?"; + if (name != null){ + selectionArgs.add(name); + selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?"; + } + if (fingerprint != null){ + selectionArgs.add(fingerprint); + selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?"; + } + if (own != null){ + selectionArgs.add(own?"1":"0"); + selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?"; + } + Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, + columns, + selectionString, + selectionArgs.toArray(new String[selectionArgs.size()]), + null, null, null); + + return cursor; + } + + public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) { + IdentityKeyPair identityKeyPair = null; + Cursor cursor = getIdentityKeyCursor(account, name, true); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + } + } + cursor.close(); + + return identityKeyPair; + } + + public Set<IdentityKey> loadIdentityKeys(Account account, String name) { + return loadIdentityKeys(account, name, null); + } + + public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) { + 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()) { + continue; + } + try { + identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); + } + } + cursor.close(); + + return identityKeys; + } + + public long numTrustedKeys(Account account, String name) { + SQLiteDatabase db = getReadableDatabase(); + String[] args = { + account.getUuid(), + name, + String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()) + }; + return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?" + + " AND " + SQLiteAxolotlStore.NAME + " = ?" + + " AND " + SQLiteAxolotlStore.TRUSTED + " = ?", + 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) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(SQLiteAxolotlStore.NAME, name); + values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0); + values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); + values.put(SQLiteAxolotlStore.KEY, base64Serialized); + values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode()); + db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + } + + public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) { + Cursor cursor = getIdentityKeyCursor(account, fingerprint); + XmppAxolotlSession.Trust trust = null; + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED)); + trust = XmppAxolotlSession.Trust.fromCode(trustValue); + } + cursor.close(); + return trust; + } + + public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode()); + int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs); + return rows == 1; + } + + 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 storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { + storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED); + } + + public void recreateAxolotlDb() { + recreateAxolotlDb(getWritableDatabase()); + } + + public void recreateAxolotlDb(SQLiteDatabase db) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<"); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL(CREATE_IDENTITIES_STATEMENT); + } + + public void wipeAxolotlDb(Account account) { + String accountName = account.getUuid(); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + SQLiteDatabase db = this.getWritableDatabase(); + String[] deleteArgs= { + accountName + }; + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + } } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index ab191285..9381ba1d 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1,5 +1,19 @@ package eu.siacs.conversations.persistance; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; +import android.webkit.MimeTypeMap; + import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -8,6 +22,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.Socket; import java.net.URL; import java.security.DigestOutputStream; import java.security.MessageDigest; @@ -17,28 +32,15 @@ import java.util.Arrays; import java.util.Date; import java.util.Locale; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.net.Uri; -import android.os.Environment; -import android.provider.MediaStore; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.Log; -import android.webkit.MimeTypeMap; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; 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.xmpp.pep.Avatar; public class FileBackend { @@ -126,25 +128,24 @@ public class FileBackend { return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); } - public String getOriginalPath(Uri uri) { - String path = null; - if (uri.getScheme().equals("file")) { - return uri.getPath(); - } else if (uri.toString().startsWith("content://media/")) { - String[] projection = {MediaStore.MediaColumns.DATA}; - Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri, - projection, null, null, null); - if (metaCursor != null) { - try { - if (metaCursor.moveToFirst()) { - path = metaCursor.getString(0); - } - } finally { - metaCursor.close(); - } - } + public boolean useImageAsIs(Uri uri) { + String path = getOriginalPath(uri); + if (path == null) { + return false; + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + try { + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options); + return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); + } catch (FileNotFoundException e) { + return false; } - return path; + } + + public String getOriginalPath(Uri uri) { + Log.d(Config.LOGTAG,"get original path for uri: "+uri.toString()); + return FileUtils.getPath(mXmppConnectionService,uri); } public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { @@ -184,8 +185,18 @@ public class FileBackend { return this.copyImageToPrivateStorage(message, image, 0); } - private DownloadableFile copyImageToPrivateStorage(Message message, - Uri image, int sampleSize) throws FileCopyException { + private DownloadableFile copyImageToPrivateStorage(Message message,Uri image, int sampleSize) throws FileCopyException { + switch(Config.IMAGE_FORMAT) { + case JPEG: + message.setRelativeFilePath(message.getUuid()+".jpg"); + break; + case PNG: + message.setRelativeFilePath(message.getUuid()+".png"); + break; + case WEBP: + message.setRelativeFilePath(message.getUuid()+".webp"); + break; + } DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); InputStream is = null; @@ -205,13 +216,13 @@ public class FileBackend { if (originalBitmap == null) { throw new FileCopyException(R.string.error_not_an_image_file); } - Bitmap scaledBitmap = resize(originalBitmap, IMAGE_SIZE); + Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE); int rotation = getRotation(image); if (rotation > 0) { scaledBitmap = rotate(scaledBitmap, rotation); } - boolean success = scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 75, os); + boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, Config.IMAGE_QUALITY, os); if (!success) { throw new FileCopyException(R.string.error_compressing_image); } @@ -346,11 +357,7 @@ public class FileBackend { file.delete(); return false; } - } catch (FileNotFoundException e) { - return false; - } catch (IOException e) { - return false; - } catch (NoSuchAlgorithmException e) { + } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) { return false; } finally { close(os); @@ -377,6 +384,9 @@ public class FileBackend { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image, size); is = mXmppConnectionService.getContentResolver().openInputStream(image); + if (is == null) { + return null; + } Bitmap input = BitmapFactory.decodeStream(is, null, options); if (input == null) { return null; @@ -403,6 +413,9 @@ public class FileBackend { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth)); is = mXmppConnectionService.getContentResolver().openInputStream(image); + if (is == null) { + return null; + } Bitmap source = BitmapFactory.decodeStream(is, null, options); if (source == null) { return null; @@ -546,4 +559,13 @@ public class FileBackend { } } } + + public static void close(Socket socket) { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + } + } + } } diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 676a09c9..5def05dd 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -1,5 +1,35 @@ package eu.siacs.conversations.services; +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.DownloadableFile; + public class AbstractConnectionManager { protected XmppConnectionService mXmppConnectionService; @@ -20,4 +50,75 @@ public class AbstractConnectionManager { return 524288; } } + + public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) throws FileNotFoundException { + FileInputStream is; + int size; + is = new FileInputStream(file); + size = (int) file.getSize(); + if (file.getKey() == null) { + return new Pair<InputStream,Integer>(is,size); + } + try { + if (gcm) { + AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); + InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher); + return new Pair<>(cis, cipher.getOutputSize(size)); + } else { + IvParameterSpec ips = new IvParameterSpec(file.getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + final int s = Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE ? size : (size / 16 + 1) * 16; + return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),s); + } + } catch (InvalidKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (NoSuchPaddingException e) { + return null; + } catch (InvalidAlgorithmParameterException e) { + return null; + } + } + + public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) { + FileOutputStream os; + try { + os = new FileOutputStream(file); + if (file.getKey() == null) { + return os; + } + } catch (FileNotFoundException e) { + return null; + } + try { + if (gcm) { + AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); + return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher); + } else { + IvParameterSpec ips = new IvParameterSpec(file.getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(os, cipher); + } + } catch (InvalidKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (NoSuchPaddingException e) { + return null; + } catch (InvalidAlgorithmParameterException e) { + return null; + } + } + + public PowerManager.WakeLock createWakeLock(String name) { + PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE); + return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name); + } } diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java index dfbe9db7..ceab1592 100644 --- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java +++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java @@ -1,10 +1,11 @@ package eu.siacs.conversations.services; -import eu.siacs.conversations.persistance.DatabaseBackend; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import eu.siacs.conversations.persistance.DatabaseBackend; + public class EventReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 0bc428c8..a31848a7 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -111,7 +111,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() != IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); finalizeQuery(query); } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 683fcb04..d03dcb84 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -65,7 +65,7 @@ public class NotificationService { return (message.getStatus() == Message.STATUS_RECEIVED) && notificationsEnabled() && !message.getConversation().isMuted() - && (message.getConversation().getMode() == Conversation.MODE_SINGLE + && (message.getConversation().isPnNA() || conferenceNotificationsEnabled() || wasHighlightedOrPrivate(message) ); @@ -333,9 +333,10 @@ public class NotificationService { private Message getImage(final Iterable<Message> messages) { for (final Message message : messages) { - if (message.getType() == Message.TYPE_IMAGE + if (message.getType() != Message.TYPE_TEXT && message.getTransferable() == null - && message.getEncryption() != Message.ENCRYPTION_PGP) { + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getFileParams().height > 0) { return message; } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f50b9fa6..b165ede4 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -53,16 +53,17 @@ import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.PresenceGenerator; @@ -86,6 +87,7 @@ import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; @@ -213,7 +215,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() != IqPacket.TYPE.RESULT) { Element error = packet.findChild("error"); String text = error != null ? error.findChildContent("text") : null; if (text != null) { @@ -274,11 +276,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } syncDirtyContacts(account); - scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode()); + account.getAxolotlService().publishOwnDeviceIdIfNeeded(); + account.getAxolotlService().publishBundlesIfNeeded(); + + scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { resetSendingToWaiting(account); if (!account.isOptionSet(Account.OPTION_DISABLED)) { - int timeToReconnect = mRandom.nextInt(50) + 10; + int timeToReconnect = mRandom.nextInt(20) + 10; scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode()); } } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { @@ -305,6 +310,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private int rosterChangedListenerCount = 0; private OnMucRosterUpdate mOnMucRosterUpdate = null; private int mucRosterChangedListenerCount = 0; + private OnKeyStatusUpdated mOnKeyStatusUpdated = null; + private int keyStatusUpdatedListenerCount = 0; private SecureRandom mRandom; private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; @@ -343,7 +350,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { - int encryption = conversation.getNextEncryption(forceEncryption()); + int encryption = conversation.getNextEncryption(); if (encryption == Message.ENCRYPTION_PGP) { encryption = Message.ENCRYPTION_DECRYPTED; } @@ -362,12 +369,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final Uri uri, final UiCallback<Message> callback) { final Message message; - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption())); + message = new Message(conversation, "", conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); @@ -382,33 +387,35 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } else { mFileAddingExecutor.execute(new Runnable() { - @Override - public void run() { - try { - getFileBackend().copyFileToPrivateStorage(message, uri); - getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); + @Override + public void run() { + try { + getFileBackend().copyFileToPrivateStorage(message, uri); + getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.FileCopyException e) { + callback.error(e.getResId(), message); + } + } + }); } } - public void attachImageToConversation(final Conversation conversation, - final Uri uri, final UiCallback<Message> callback) { + public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { + if (getFileBackend().useImageAsIs(uri)) { + Log.d(Config.LOGTAG,"using image as is"); + attachFileToConversation(conversation, uri, callback); + return; + } final Message message; - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption())); + message = new Message(conversation, "",conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_IMAGE); @@ -418,7 +425,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void run() { try { getFileBackend().copyImageToPrivateStorage(message, uri); - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); @@ -611,9 +618,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); - for (final Account account : this.accounts) { - account.initAccountServices(this); - } restoreFromDatabase(); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); @@ -694,22 +698,22 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } - private void sendFileMessage(final Message message) { + private void sendFileMessage(final Message message, final boolean delay) { Log.d(Config.LOGTAG, "send file message"); final Account account = message.getConversation().getAccount(); final XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().httpUpload()) { - mHttpConnectionManager.createNewUploadConnection(message); + mHttpConnectionManager.createNewUploadConnection(message, delay); } else { mJingleConnectionManager.createNewConnection(message); } } public void sendMessage(final Message message) { - sendMessage(message, false); + sendMessage(message, false, false); } - private void sendMessage(final Message message, final boolean resend) { + private void sendMessage(final Message message, final boolean resend, final boolean delay) { final Account account = message.getConversation().getAccount(); final Conversation conversation = message.getConversation(); account.deactivateGracePeriod(); @@ -719,7 +723,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { message.getConversation().endOtrIfNeeded(); - message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, + new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { markMessage(message,Message.STATUS_SEND_FAILED); @@ -732,24 +737,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa case Message.ENCRYPTION_NONE: if (message.needsUploading()) { if (account.httpUploadAvailable() || message.fixCounterpart()) { - this.sendFileMessage(message); + this.sendFileMessage(message,delay); } else { break; } } else { - packet = mMessageGenerator.generateChat(message,resend); + packet = mMessageGenerator.generateChat(message); } break; case Message.ENCRYPTION_PGP: case Message.ENCRYPTION_DECRYPTED: if (message.needsUploading()) { if (account.httpUploadAvailable() || message.fixCounterpart()) { - this.sendFileMessage(message); + this.sendFileMessage(message,delay); } else { break; } } else { - packet = mMessageGenerator.generatePgpChat(message,resend); + packet = mMessageGenerator.generatePgpChat(message); } break; case Message.ENCRYPTION_OTR: @@ -763,7 +768,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.needsUploading()) { mJingleConnectionManager.createNewConnection(message); } else { - packet = mMessageGenerator.generateOtrChat(message,resend); + packet = mMessageGenerator.generateOtrChat(message); } } else if (otrSession == null) { if (message.fixCounterpart()) { @@ -773,6 +778,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } break; + case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + if (message.needsUploading()) { + if (account.httpUploadAvailable() || message.fixCounterpart()) { + this.sendFileMessage(message,delay); + } else { + break; + } + } else { + XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message); + if (axolotlMessage == null) { + account.getAxolotlService().preparePayloadMessage(message, delay); + } else { + packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage); + } + } + break; + } if (packet != null) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { @@ -800,6 +823,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa conversation.startOtrSession(message.getCounterpart().getResourcepart(), false); } break; + case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + break; } } @@ -819,6 +845,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateConversationUi(); } if (packet != null) { + if (delay) { + mMessageGenerator.addDelay(packet,message.getTimeSent()); + } if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { if (this.sendChatStates()) { packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); @@ -833,13 +862,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onMessageFound(Message message) { - resendMessage(message); + resendMessage(message, true); } }); } - public void resendMessage(final Message message) { - sendMessage(message, true); + public void resendMessage(final Message message, final boolean delay) { + sendMessage(message, true, delay); } public void fetchRosterFromServer(final Account account) { @@ -850,8 +879,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } - iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion()); - sendIqPacket(account, iqPacket, mIqParser); + iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion()); + sendIqPacket(account,iqPacket,mIqParser); } public void fetchBookmarks(final Account account) { @@ -862,28 +891,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { - final Element query = packet.query(); - final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); - final Element storage = query.findChild("storage", - "storage:bookmarks"); - if (storage != null) { - for (final Element item : storage.getChildren()) { - if (item.getName().equals("conference")) { - final Bookmark bookmark = Bookmark.parse(item, account); - bookmarks.add(bookmark); - Conversation conversation = find(bookmark); - if (conversation != null) { - conversation.setBookmark(bookmark); - } else if (bookmark.autojoin() && bookmark.getJid() != null) { - conversation = findOrCreateConversation( - account, bookmark.getJid(), true); - conversation.setBookmark(bookmark); - joinMuc(conversation); + if (packet.getType() == IqPacket.TYPE.RESULT) { + final Element query = packet.query(); + final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); + final Element storage = query.findChild("storage", "storage:bookmarks"); + if (storage != null) { + for (final Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + final Bookmark bookmark = Bookmark.parse(item, account); + bookmarks.add(bookmark); + Conversation conversation = find(bookmark); + if (conversation != null) { + conversation.setBookmark(bookmark); + } else if (bookmark.autojoin() && bookmark.getJid() != null) { + conversation = findOrCreateConversation( + account, bookmark.getJid(), true); + conversation.setBookmark(bookmark); + joinMuc(conversation); + } } } } + account.setBookmarks(bookmarks); + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fetch bookmarks"); } - account.setBookmarks(bookmarks); } }; sendIqPacket(account, iqPacket, callback); @@ -963,6 +995,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Log.d(Config.LOGTAG,"restoring roster"); for(Account account : accounts) { databaseBackend.readRoster(account.getRoster()); + account.initAccountServices(XmppConnectionService.this); } getBitmapCache().evictAll(); Looper.prepare(); @@ -994,6 +1027,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void onMessageFound(Message message) { if (!getFileBackend().isFileAvailable(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); + final int s = message.getStatus(); + if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) { + markMessage(message,Message.STATUS_SEND_FAILED); + } } } }); @@ -1005,7 +1042,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message != null) { if (!getFileBackend().isFileAvailable(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - updateConversationUi(); + final int s = message.getStatus(); + if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) { + markMessage(message,Message.STATUS_SEND_FAILED); + } else { + updateConversationUi(); + } } return; } @@ -1029,19 +1071,19 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } Collections.sort(list, new Comparator<Conversation>() { - @Override - public int compare(Conversation lhs, Conversation rhs) { - Message left = lhs.getLatestMessage(); - Message right = rhs.getLatestMessage(); - if (left.getTimeSent() > right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() < right.getTimeSent()) { - return 1; - } else { - return 0; - } - } - }); + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); } public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { @@ -1370,6 +1412,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnKeyStatusUpdated = listener; + if (this.keyStatusUpdatedListenerCount < 2) { + this.keyStatusUpdatedListenerCount++; + } + } + } + + public void removeOnNewKeysAvailableListener() { + synchronized (this) { + this.keyStatusUpdatedListenerCount--; + if (this.keyStatusUpdatedListenerCount <= 0) { + this.keyStatusUpdatedListenerCount = 0; + this.mOnKeyStatusUpdated = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { synchronized (this) { if (checkListeners()) { @@ -1400,7 +1467,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null && this.mOnUpdateBlocklist == null - && this.mOnShowErrorToast == null); + && this.mOnShowErrorToast == null + && this.mOnKeyStatusUpdated == null); } private void switchToForeground() { @@ -1638,7 +1706,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { + if (packet.getType() == IqPacket.TYPE.RESULT) { ArrayList<String> features = new ArrayList<>(); for (Element child : packet.query().getChildren()) { if (child != null && child.getName().equals("feature")) { @@ -1662,7 +1730,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { + if (packet.getType() == IqPacket.TYPE.RESULT) { Data data = Data.parse(packet.query().findChild("x", "jabber:x:data")); for (Field field : data.getFields()) { if (options.containsKey(field.getName())) { @@ -1676,12 +1744,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa sendIqPacket(account, set, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (callback != null) { + if (callback != null) { + if (packet.getType() == IqPacket.TYPE.RESULT) { callback.onPushSucceeded(); - } - } else { - if (callback != null) { + } else { callback.onPushFailed(); } } @@ -1812,7 +1878,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa account.getJid().toBareJid() + " otr session established with " + conversation.getJid() + "/" + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { @@ -1825,8 +1891,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.needsUploading()) { mJingleConnectionManager.createNewConnection(message); } else { - MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); + MessagePacket outPacket = mMessageGenerator.generateOtrChat(message); if (outPacket != null) { + mMessageGenerator.addDelay(outPacket, message.getTimeSent()); message.setStatus(Message.STATUS_SEND); databaseBackend.updateMessage(message); sendMessagePacket(account, outPacket); @@ -1917,10 +1984,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final IqPacket packet = XmppConnectionService.this.mIqGenerator .publishAvatarMetadata(avatar); sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, - IqPacket result) { + public void onIqPacketReceived(Account account, IqPacket result) { if (result.getType() == IqPacket.TYPE.RESULT) { if (account.setAvatar(avatar.getFilename())) { getAvatarService().clear(account); @@ -2226,6 +2291,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public boolean forceEncryption() { return getPreferences().getBoolean("force_encryption", false); } + public boolean confirmMessages() { return getPreferences().getBoolean("confirm_messages", true); } @@ -2287,6 +2353,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void keyStatusUpdated() { + if(mOnKeyStatusUpdated != null) { + mOnKeyStatusUpdated.onKeyStatusUpdated(); + } + } + public Account findAccountByJid(final Jid accountJid) { for (Account account : this.accounts) { if (account.getJid().toBareJid().equals(accountJid.toBareJid())) { @@ -2497,8 +2569,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } for (final Message msg : messages) { + msg.setTime(System.currentTimeMillis()); markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg); + this.resendMessage(msg,false); } } diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java index a57e1b89..bd2042fb 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.ui; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.preference.Preference; import android.util.AttributeSet; diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java index 13d7f4fc..08594201 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java @@ -55,16 +55,10 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem } Collections.sort(getListItems()); } - runOnUiThread(new Runnable() { - @Override - public void run() { - getListItemAdapter().notifyDataSetChanged(); - } - }); + getListItemAdapter().notifyDataSetChanged(); } - @Override - public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { + protected void refreshUiReal() { final Editable editable = getSearchEditText().getText(); if (editable != null) { filterContacts(editable.toString()); @@ -72,4 +66,9 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem filterContacts(); } } + + @Override + public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { + refreshUi(); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java index aac435fd..09988b7c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java @@ -4,7 +4,6 @@ import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import eu.siacs.conversations.R; @@ -107,4 +106,8 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti }); } + + public void refreshUiReal() { + + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index c9e99ce5..bc23c3ba 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -13,11 +13,11 @@ import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.ListView; -import java.util.Set; -import java.util.HashSet; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.ArrayList; +import java.util.Set; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -149,4 +149,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity { return result.toArray(new String[result.size()]); } + + public void refreshUiReal() { + //nothing to do. This Activity doesn't implement any listeners + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 07b8819d..12b66c35 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -27,8 +27,8 @@ import org.openintents.openpgp.util.OpenPgpUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; @@ -38,8 +38,8 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; import eu.siacs.conversations.xmpp.jid.Jid; public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed { @@ -266,14 +266,17 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final User self = mConversation.getMucOptions().getSelf(); this.mSelectedUser = user; String name; + final Contact contact = user.getContact(); + if (contact != null) { + name = contact.getDisplayName(); + } else if (user.getJid() != null){ + name = user.getJid().toBareJid().toString(); + } else { + name = user.getName(); + } + menu.setHeaderTitle(name); if (user.getJid() != null) { - final Contact contact = user.getContact(); - if (contact != null) { - name = contact.getDisplayName(); - } else { - name = user.getJid().toBareJid().toString(); - } - menu.setHeaderTitle(name); + MenuItem showContactDetails = menu.findItem(R.id.action_contact_details); MenuItem startConversation = menu.findItem(R.id.start_conversation); MenuItem giveMembership = menu.findItem(R.id.give_membership); MenuItem removeMembership = menu.findItem(R.id.remove_membership); @@ -282,6 +285,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room); MenuItem banFromConference = menu.findItem(R.id.ban_from_conference); startConversation.setVisible(true); + if (contact != null) { + showContactDetails.setVisible(true); + } if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) { if (mAdvancedMode) { @@ -300,15 +306,24 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers removeAdminPrivileges.setVisible(true); } } + } else { + MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message); + sendPrivateMessage.setVisible(true); } } - super.onCreateContextMenu(menu,v,menuInfo); + super.onCreateContextMenu(menu, v, menuInfo); } @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.action_contact_details: + Contact contact = mSelectedUser.getContact(); + if (contact != null) { + switchToContactDetails(contact); + } + return true; case R.id.start_conversation: startConversation(mSelectedUser); return true; @@ -331,6 +346,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this); xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this); return true; + case R.id.send_private_message: + privateMsgInMuc(mConversation,mSelectedUser.getName()); + return true; default: return super.onContextItemSelected(item); } @@ -404,8 +422,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers private void updateView() { final MucOptions mucOptions = mConversation.getMucOptions(); final User self = mucOptions.getSelf(); - mAccountJid.setText(getString(R.string.using_account, mConversation - .getAccount().getJid().toBareJid())); + String account; + if (Config.DOMAIN_LOCK != null) { + account = mConversation.getAccount().getJid().getLocalpart(); + } else { + account = mConversation.getAccount().getJid().toBareJid().toString(); + } + mAccountJid.setText(getString(R.string.using_account, account)); mYourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48))); setTitle(mConversation.getName()); mFullJid.setText(mConversation.getJid().toBareJid().toString()); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index c190caed..d98e9164 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -29,9 +29,11 @@ import android.widget.QuickContactBadge; import android.widget.TextView; import org.openintents.openpgp.util.OpenPgpUtils; +import org.whispersystems.libaxolotl.IdentityKey; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; @@ -41,12 +43,13 @@ 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.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 { +public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated { public static final String ACTION_VIEW_CONTACT = "view_contact"; private Contact contact; @@ -108,6 +111,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd private LinearLayout keys; private LinearLayout tags; private boolean showDynamicTags; + private String messageFingerprint; private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { @@ -158,6 +162,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } @Override + public void OnUpdateBlocklist(final Status status) { + refreshUi(); + } + + @Override protected void refreshUiReal() { invalidateOptionsMenu(); populateView(); @@ -185,6 +194,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } catch (final InvalidJidException ignored) { } } + this.messageFingerprint = getIntent().getStringExtra("fingerprint"); setContentView(R.layout.activity_contact_details); contactJidTv = (TextView) findViewById(R.id.details_contactjid); @@ -350,7 +360,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } else { contactJidTv.setText(contact.getJid().toString()); } - accountJidTv.setText(getString(R.string.using_account, contact.getAccount().getJid().toBareJid())); + String account; + if (Config.DOMAIN_LOCK != null) { + account = contact.getAccount().getJid().getLocalpart(); + } else { + account = contact.getAccount().getJid().toBareJid().toString(); + } + accountJidTv.setText(getString(R.string.using_account, account)); badge.setImageBitmap(avatarService().get(contact, getPixel(72))); badge.setOnClickListener(this.onBadgeClick); @@ -362,13 +378,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); - ImageButton remove = (ImageButton) view + ImageButton removeButton = (ImageButton) view .findViewById(R.id.button_remove); - remove.setVisibility(View.VISIBLE); + removeButton.setVisibility(View.VISIBLE); keyType.setText("OTR Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); keys.addView(view); - remove.setOnClickListener(new OnClickListener() { + removeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -376,6 +392,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } }); } + for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( + contact.getAccount(), contact.getJid().toBareJid().toString())) { + boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint); + hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey, highlight); + } if (contact.getPgpKeyId() != 0) { hasKeys = true; View view = inflater.inflate(R.layout.contact_key, keys, false); @@ -460,14 +481,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } @Override - public void OnUpdateBlocklist(final Status status) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - invalidateOptionsMenu(); - populateView(); - } - }); + public void onKeyStatusUpdated() { + refreshUi(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index bf261dba..7fec827b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -16,6 +16,7 @@ import android.os.Bundle; import android.provider.MediaStore; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -28,13 +29,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.Toast; import net.java.otr4j.session.SessionStatus; -import de.timroes.android.listview.EnhancedListView; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +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.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; @@ -47,6 +51,8 @@ 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.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; import github.ankushsachdeva.emojicon.EmojiconEditText; public class ConversationActivity extends XmppActivity @@ -59,15 +65,19 @@ public class ConversationActivity extends XmppActivity public static final String MESSAGE = "messageUuid"; public static final String TEXT = "text"; public static final String NICK = "nick"; + public static final String PRIVATE_MESSAGE = "pm"; public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; + public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208; + public static final int REQUEST_TRUST_KEYS_MENU = 0x0209; public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; + public static final int ATTACHMENT_CHOICE_INVALID = 0x0306; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -77,6 +87,7 @@ public class ConversationActivity extends XmppActivity final private List<Uri> mPendingImageUris = new ArrayList<>(); final private List<Uri> mPendingFileUris = new ArrayList<>(); private Uri mPendingGeoUri = null; + private boolean forbidProcessingPendings = false; private View mContentView; @@ -188,64 +199,64 @@ public class ConversationActivity extends XmppActivity listView.setDismissCallback(new EnhancedListView.OnDismissCallback() { - @Override - public EnhancedListView.Undoable onDismiss(final EnhancedListView enhancedListView, final int position) { - - final int index = listView.getFirstVisiblePosition(); - View v = listView.getChildAt(0); - final int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); - - swipedConversation = listAdapter.getItem(position); - listAdapter.remove(swipedConversation); - swipedConversation.markRead(); - xmppConnectionService.getNotificationService().clear(swipedConversation); - - final boolean formerlySelected = (getSelectedConversation() == swipedConversation); - if (position == 0 && listAdapter.getCount() == 0) { - endConversation(swipedConversation, false, true); - return null; - } else if (formerlySelected) { - setSelectedConversation(listAdapter.getItem(0)); - ConversationActivity.this.mConversationFragment - .reInit(getSelectedConversation()); - } - - return new EnhancedListView.Undoable() { - - @Override - public void undo() { - listAdapter.insert(swipedConversation, position); - if (formerlySelected) { - setSelectedConversation(swipedConversation); - ConversationActivity.this.mConversationFragment - .reInit(getSelectedConversation()); - } - swipedConversation = null; - listView.setSelectionFromTop(index + (listView.getChildCount() < position ? 1 : 0), top); - } - - @Override - public void discard() { - if (!swipedConversation.isRead() - && swipedConversation.getMode() == Conversation.MODE_SINGLE) { - swipedConversation = null; - return; - } - endConversation(swipedConversation, false, false); - swipedConversation = null; - } - - @Override - public String getTitle() { - if (swipedConversation.getMode() == Conversation.MODE_MULTI) { - return getResources().getString(R.string.title_undo_swipe_out_muc); - } else { - return getResources().getString(R.string.title_undo_swipe_out_conversation); - } - } - }; - } - }); + @Override + public EnhancedListView.Undoable onDismiss(final EnhancedListView enhancedListView, final int position) { + + final int index = listView.getFirstVisiblePosition(); + View v = listView.getChildAt(0); + final int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); + + swipedConversation = listAdapter.getItem(position); + listAdapter.remove(swipedConversation); + swipedConversation.markRead(); + xmppConnectionService.getNotificationService().clear(swipedConversation); + + final boolean formerlySelected = (getSelectedConversation() == swipedConversation); + if (position == 0 && listAdapter.getCount() == 0) { + endConversation(swipedConversation, false, true); + return null; + } else if (formerlySelected) { + setSelectedConversation(listAdapter.getItem(0)); + ConversationActivity.this.mConversationFragment + .reInit(getSelectedConversation()); + } + + return new EnhancedListView.Undoable() { + + @Override + public void undo() { + listAdapter.insert(swipedConversation, position); + if (formerlySelected) { + setSelectedConversation(swipedConversation); + ConversationActivity.this.mConversationFragment + .reInit(getSelectedConversation()); + } + swipedConversation = null; + listView.setSelectionFromTop(index + (listView.getChildCount() < position ? 1 : 0), top); + } + + @Override + public void discard() { + if (!swipedConversation.isRead() + && swipedConversation.getMode() == Conversation.MODE_SINGLE) { + swipedConversation = null; + return; + } + endConversation(swipedConversation, false, false); + swipedConversation = null; + } + + @Override + public String getTitle() { + if (swipedConversation.getMode() == Conversation.MODE_MULTI) { + return getResources().getString(R.string.title_undo_swipe_out_muc); + } else { + return getResources().getString(R.string.title_undo_swipe_out_conversation); + } + } + }; + } + }); listView.enableSwipeToDismiss(); listView.setSwipeDirection(EnhancedListView.SwipeDirection.START); listView.setSwipingLayout(R.id.swipeable_item); @@ -376,7 +387,7 @@ public class ConversationActivity extends XmppActivity } else { menuAdd.setVisible(!isConversationsOverviewHideable()); if (this.getSelectedConversation() != null) { - if (this.getSelectedConversation().getNextEncryption(forceEncryption()) != Message.ENCRYPTION_NONE) { + if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { menuSecure.setIcon(R.drawable.ic_lock_white_24dp); } else { @@ -387,6 +398,7 @@ public class ConversationActivity extends XmppActivity menuContactDetails.setVisible(false); menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable()); menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); + menuSecure.setVisible(!Config.HIDE_PGP_IN_UI); //if pgp is hidden conferences have no choice of encryption } else { menuMucDetails.setVisible(false); } @@ -400,7 +412,7 @@ public class ConversationActivity extends XmppActivity return true; } - private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { + protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { final Conversation conversation = getSelectedConversation(); final Account account = conversation.getAccount(); final OnPresenceSelected callback = new OnPresenceSelected() { @@ -489,7 +501,7 @@ public class ConversationActivity extends XmppActivity break; } final Conversation conversation = getSelectedConversation(); - final int encryption = conversation.getNextEncryption(forceEncryption()); + final int encryption = conversation.getNextEncryption(); if (encryption == Message.ENCRYPTION_PGP) { if (hasPgp()) { if (conversation.getContact().getPgpKeyId() != 0) { @@ -536,7 +548,9 @@ public class ConversationActivity extends XmppActivity showInstallPgpDialog(); } } else { - selectPresenceToAttachFile(attachmentChoice, encryption); + if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) { + selectPresenceToAttachFile(attachmentChoice, encryption); + } } } @@ -701,13 +715,13 @@ public class ConversationActivity extends XmppActivity intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); switch (menuItem.getItemId()) { case R.id.scan_fingerprint: - intent.putExtra("mode", VerifyOTRActivity.MODE_SCAN_FINGERPRINT); + intent.putExtra("mode",VerifyOTRActivity.MODE_SCAN_FINGERPRINT); break; case R.id.ask_question: - intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION); + intent.putExtra("mode",VerifyOTRActivity.MODE_ASK_QUESTION); break; case R.id.manual_verification: - intent.putExtra("mode", VerifyOTRActivity.MODE_MANUAL_VERIFICATION); + intent.putExtra("mode",VerifyOTRActivity.MODE_MANUAL_VERIFICATION); break; } startActivity(intent); @@ -751,6 +765,12 @@ public class ConversationActivity extends XmppActivity showInstallPgpDialog(); } break; + case R.id.encryption_choice_axolotl: + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) + + "Enabled axolotl for Contact " + conversation.getContact().getJid()); + conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); + item.setChecked(true); + break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); break; @@ -758,6 +778,7 @@ public class ConversationActivity extends XmppActivity xmppConnectionService.databaseBackend.updateConversation(conversation); fragment.updateChatMsgHint(); invalidateOptionsMenu(); + refreshUi(); return true; } }); @@ -765,14 +786,15 @@ public class ConversationActivity extends XmppActivity MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr); MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none); MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp); + MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl); + pgp.setVisible(!Config.HIDE_PGP_IN_UI); if (conversation.getMode() == Conversation.MODE_MULTI) { - otr.setEnabled(false); - } else { - if (forceEncryption()) { - none.setVisible(false); - } + otr.setVisible(false); + axolotl.setVisible(false); + } else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { + axolotl.setEnabled(false); } - switch (conversation.getNextEncryption(forceEncryption())) { + switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: none.setChecked(true); break; @@ -782,6 +804,9 @@ public class ConversationActivity extends XmppActivity case Message.ENCRYPTION_PGP: pgp.setChecked(true); break; + case Message.ENCRYPTION_AXOLOTL: + axolotl.setChecked(true); + break; default: none.setChecked(true); break; @@ -793,8 +818,7 @@ public class ConversationActivity extends XmppActivity protected void muteConversationDialog(final Conversation conversation) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.disable_notifications); - final int[] durations = getResources().getIntArray( - R.array.mute_options_durations); + final int[] durations = getResources().getIntArray(R.array.mute_options_durations); builder.setItems(R.array.mute_options_descriptions, new OnClickListener() { @@ -808,7 +832,7 @@ public class ConversationActivity extends XmppActivity } conversation.setMutedTill(till); ConversationActivity.this.xmppConnectionService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); updateConversationList(); ConversationActivity.this.mConversationFragment.updateMessages(); invalidateOptionsMenu(); @@ -946,18 +970,23 @@ public class ConversationActivity extends XmppActivity this.mConversationFragment.reInit(getSelectedConversation()); } - for(Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { - attachImageToConversation(getSelectedConversation(),i.next()); - } + if(!forbidProcessingPendings) { + for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { + Uri foo = i.next(); + attachImageToConversation(getSelectedConversation(), foo); + } - for(Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { - attachFileToConversation(getSelectedConversation(),i.next()); - } + for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { + attachFileToConversation(getSelectedConversation(), i.next()); + } - if (mPendingGeoUri != null) { - attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); - mPendingGeoUri = null; + if (mPendingGeoUri != null) { + attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); + mPendingGeoUri = null; + } } + forbidProcessingPendings = false; + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); setIntent(new Intent()); } @@ -967,10 +996,21 @@ public class ConversationActivity extends XmppActivity final String downloadUuid = intent.getStringExtra(MESSAGE); final String text = intent.getStringExtra(TEXT); final String nick = intent.getStringExtra(NICK); + final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE,false); if (selectConversationByUuid(uuid)) { this.mConversationFragment.reInit(getSelectedConversation()); if (nick != null) { - this.mConversationFragment.highlightInConference(nick); + if (pm) { + Jid jid = getSelectedConversation().getJid(); + try { + Jid next = Jid.fromParts(jid.getLocalpart(),jid.getDomainpart(),nick); + this.mConversationFragment.privateMessageWith(next); + } catch (final InvalidJidException ignored) { + //do nothing + } + } else { + this.mConversationFragment.highlightInConference(nick); + } } else { this.mConversationFragment.appendText(text); } @@ -1067,6 +1107,9 @@ public class ConversationActivity extends XmppActivity attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); this.mPendingGeoUri = null; } + } else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) { + this.forbidProcessingPendings = !xmppConnectionServiceBound; + mConversationFragment.onActivityResult(requestCode, resultCode, data); } } else { mPendingImageUris.clear(); @@ -1207,10 +1250,6 @@ public class ConversationActivity extends XmppActivity }); } - public boolean forceEncryption() { - return getPreferences().getBoolean("force_encryption", false); - } - public boolean useSendButtonToIndicateStatus() { return getPreferences().getBoolean("send_button_status", false); } @@ -1219,6 +1258,30 @@ public class ConversationActivity extends XmppActivity return getPreferences().getBoolean("indicate_received", false); } + protected boolean trustKeysIfNeeded(int requestCode) { + return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID); + } + + protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) { + AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService(); + boolean hasPendingKeys = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, + mSelectedConversation.getContact()).isEmpty() + || !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty(); + boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0; + if( hasPendingKeys || hasNoTrustedKeys) { + axolotlService.createSessionsIfNeeded(mSelectedConversation); + Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class); + intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString()); + intent.putExtra("choice", attachmentChoice); + intent.putExtra("has_no_trusted", hasNoTrustedKeys); + startActivityForResult(intent, requestCode); + return true; + } else { + return false; + } + } + @Override protected void refreshUiReal() { updateConversationList(); @@ -1240,6 +1303,7 @@ public class ConversationActivity extends XmppActivity ConversationActivity.this.mConversationFragment.updateMessages(); updateActionBarTitle(); } + invalidateOptionsMenu(); } @Override @@ -1260,12 +1324,6 @@ public class ConversationActivity extends XmppActivity @Override public void OnUpdateBlocklist(Status status) { this.refreshUi(); - runOnUiThread(new Runnable() { - @Override - public void run() { - invalidateOptionsMenu(); - } - }); } public void unblockConversation(final Blockable conversation) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index eb2688fd..f645b241 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.ui; +import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.app.PendingIntent; @@ -48,12 +49,18 @@ import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +<<<<<<< HEAD import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.TransferablePlaceholder; +======= +import eu.siacs.conversations.entities.DownloadableFile; +>>>>>>> 1.6.3 import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; @@ -117,6 +124,96 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private RelativeLayout snackbar; private TextView snackbarMessage; private TextView snackbarAction; + private boolean messagesLoaded = true; + private Toast messageLoaderToast; + + private OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + + } + + private int getIndexOf(String uuid, List<Message> messages) { + if (uuid == null) { + return 0; + } + for(int i = 0; i < messages.size(); ++i) { + if (uuid.equals(messages.get(i).getUuid())) { + return i; + } else { + Message next = messages.get(i); + while(next != null && next.wasMergedIntoPrevious()) { + if (uuid.equals(next.getUuid())) { + return i; + } + next = next.next(); + } + + } + } + return 0; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + synchronized (ConversationFragment.this.messageList) { + if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { + long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); + messagesLoaded = false; + activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { + @Override + public void onMoreMessagesLoaded(final int c, Conversation conversation) { + if (ConversationFragment.this.conversation != conversation) { + return; + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + final int oldPosition = messagesView.getFirstVisiblePosition(); + Message message = messageList.get(oldPosition); + String uuid = message != null ? message.getUuid() : null; + View v = messagesView.getChildAt(0); + final int pxOffset = (v == null) ? 0 : v.getTop(); + ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); + updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + int pos = getIndexOf(uuid,messageList); + messagesView.setSelectionFromTop(pos, pxOffset); + messagesLoaded = true; + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + } + }); + } + + @Override + public void informUser(final int resId) { + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + if (ConversationFragment.this.conversation != conversation) { + return; + } + messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG); + messageLoaderToast.show(); + } + }); + + } + }); + + } + } + } + }; private IntentSender askForPassphraseIntent = null; protected OnClickListener clickToDecryptListener = new OnClickListener() { @@ -212,19 +309,27 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (body.length() == 0 || this.conversation == null) { return; } - Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); + Message message = new Message(conversation, body, conversation.getNextEncryption()); if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getNextCounterpart() != null) { message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_PRIVATE); } } - if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { - sendOtrMessage(message); - } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { - sendPgpMessage(message); - } else { - sendPlainTextMessage(message); + switch (conversation.getNextEncryption()) { + case Message.ENCRYPTION_OTR: + sendOtrMessage(message); + break; + case Message.ENCRYPTION_PGP: + sendPgpMessage(message); + break; + case Message.ENCRYPTION_AXOLOTL: + if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) { + sendAxolotlMessage(message); + } + break; + default: + sendPlainTextMessage(message); } } @@ -232,10 +337,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResourcepart())); + R.string.send_private_message_to, + conversation.getNextCounterpart().getResourcepart())); } else { - switch (conversation.getNextEncryption(activity.forceEncryption())) { + switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: mEditMessage .setHint(getString(R.string.send_plain_text_message)); @@ -243,6 +348,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case Message.ENCRYPTION_OTR: mEditMessage.setHint(getString(R.string.send_otr_message)); break; + case Message.ENCRYPTION_AXOLOTL: + mEditMessage.setHint(getString(R.string.send_omemo_message)); + break; case Message.ENCRYPTION_PGP: mEditMessage.setHint(getString(R.string.send_pgp_message)); break; @@ -388,6 +496,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa snackbarAction = (TextView) view.findViewById(R.id.snackbar_action); messagesView = (ListView) view.findViewById(R.id.messages_view); + messagesView.setOnScrollListener(mOnScrollListener); messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList); messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() { @@ -397,19 +506,20 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (message.getStatus() <= Message.STATUS_RECEIVED) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { if (message.getCounterpart() != null) { - if (!message.getCounterpart().isBareJid()) { - highlightInConference(message.getCounterpart().getResourcepart()); - } else { - highlightInConference(message.getCounterpart().toString()); + String user = message.getCounterpart().isBareJid() ? message.getCounterpart().toString() : message.getCounterpart().getResourcepart(); + if (!message.getConversation().getMucOptions().isUserInRoom(user)) { + Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user),Toast.LENGTH_SHORT).show(); } + highlightInConference(user); } } else { - activity.switchToContactDetails(message.getContact()); + activity.switchToContactDetails(message.getContact(), message.getAxolotlFingerprint()); } } else { Account account = message.getConversation().getAccount(); Intent intent = new Intent(activity, EditAccountActivity.class); intent.putExtra("jid", account.getJid().toBareJid().toString()); + intent.putExtra("fingerprint", message.getAxolotlFingerprint()); startActivity(intent); } } @@ -422,7 +532,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (message.getStatus() <= Message.STATUS_RECEIVED) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { if (message.getCounterpart() != null) { - privateMessageWith(message.getCounterpart()); + String user = message.getCounterpart().getResourcepart(); + if (user != null) { + if (message.getConversation().getMucOptions().isUserInRoom(user)) { + privateMessageWith(message.getCounterpart()); + } else { + Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user), Toast.LENGTH_SHORT).show(); + } + } } } } else { @@ -589,7 +706,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private void downloadFile(Message message) { activity.xmppConnectionService.getHttpConnectionManager() - .createNewDownloadConnection(message); + .createNewDownloadConnection(message,true); } private void cancelTransmission(Message message) { @@ -670,6 +787,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mEditMessage.setKeyboardListener(this); this.messagesView.setAdapter(messageListAdapter); updateMessages(); + this.messagesLoaded = true; int size = this.messageList.size(); if (size > 0) { messagesView.setSelection(size - 1); @@ -843,7 +961,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } catch (final NoSuchElementException ignored) { } - activity.xmppConnectionService.updateConversationUi(); + activity.refreshUi(); } }); } @@ -1001,7 +1119,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mSendButton.setImageResource(getSendButtonImageResource(action, status)); } - public void updateStatusMessages() { + protected void updateStatusMessages() { synchronized (this.messageList) { if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); @@ -1146,6 +1264,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa builder.create().show(); } + protected void sendAxolotlMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + xmppService.sendMessage(message); + messageSent(); + } + protected void sendOtrMessage(final Message message) { final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; @@ -1208,6 +1333,21 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } + @Override + public void onActivityResult(int requestCode, int resultCode, + final Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) { + final String body = mEditMessage.getText().toString(); + Message message = new Message(conversation, body, conversation.getNextEncryption()); + sendAxolotlMessage(message); + } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) { + int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID); + activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption()); + } + } + } + private void changeEmojiKeyboardIcon(ImageView iconToBeChanged, int drawableResourceId){ iconToBeChanged.setImageResource(drawableResourceId); } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 908c29d2..02b1d873 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.ui; +import android.app.AlertDialog.Builder; import android.app.PendingIntent; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; @@ -23,18 +25,24 @@ import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; +import org.whispersystems.libaxolotl.IdentityKey; + +import java.util.Set; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; 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.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.XmppConnection.Features; 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 XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated { private AutoCompleteTextView mAccountJid; private EditText mPassword; @@ -52,14 +60,23 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private TextView mServerInfoCSI; private TextView mServerInfoBlocking; private TextView mServerInfoPep; + private TextView mServerInfoHttpUpload; private TextView mSessionEst; private TextView mOtrFingerprint; + private TextView mAxolotlFingerprint; + private TextView mAccountJidLabel; private ImageView mAvatar; private RelativeLayout mOtrFingerprintBox; + private RelativeLayout mAxolotlFingerprintBox; private ImageButton mOtrFingerprintToClipboardButton; + private ImageButton mAxolotlFingerprintToClipboardButton; + private ImageButton mRegenerateAxolotlKeyButton; + private LinearLayout keys; + private LinearLayout keysCard; private Jid jidToEdit; private Account mAccount; + private String messageFingerprint; private boolean mFetchingAvatar = false; @@ -72,17 +89,34 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate xmppConnectionService.updateAccount(mAccount); return; } - final boolean registerNewAccount = mRegisterNew.isChecked(); + final boolean registerNewAccount = mRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI; + if (Config.DOMAIN_LOCK != null && mAccountJid.getText().toString().contains("@")) { + mAccountJid.setError(getString(R.string.invalid_username)); + mAccountJid.requestFocus(); + return; + } final Jid jid; try { - jid = Jid.fromString(mAccountJid.getText().toString()); + if (Config.DOMAIN_LOCK != null) { + jid = Jid.fromParts(mAccountJid.getText().toString(),Config.DOMAIN_LOCK,null); + } else { + jid = Jid.fromString(mAccountJid.getText().toString()); + } } catch (final InvalidJidException e) { - mAccountJid.setError(getString(R.string.invalid_jid)); + if (Config.DOMAIN_LOCK != null) { + mAccountJid.setError(getString(R.string.invalid_username)); + } else { + mAccountJid.setError(getString(R.string.invalid_jid)); + } mAccountJid.requestFocus(); return; } if (jid.isDomainJid()) { - mAccountJid.setError(getString(R.string.invalid_jid)); + if (Config.DOMAIN_LOCK != null) { + mAccountJid.setError(getString(R.string.invalid_username)); + } else { + mAccountJid.setError(getString(R.string.invalid_jid)); + } mAccountJid.requestFocus(); return; } @@ -108,13 +142,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); xmppConnectionService.updateAccount(mAccount); } else { - try { - if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) { - mAccountJid.setError(getString(R.string.account_already_exists)); - mAccountJid.requestFocus(); - return; - } - } catch (final InvalidJidException e) { + if (xmppConnectionService.findAccountByJid(jid) != null) { + mAccountJid.setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); return; } mAccount = new Account(jid.toBareJid(), password); @@ -139,34 +169,33 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate finish(); } }; - @Override - public void onAccountUpdate() { - runOnUiThread(new Runnable() { - @Override - public void run() { - invalidateOptionsMenu(); - if (mAccount != null - && mAccount.getStatus() != Account.State.ONLINE - && mFetchingAvatar) { - startActivity(new Intent(getApplicationContext(), - ManageAccountActivity.class)); - finish(); - } else if (jidToEdit == null && mAccount != null - && mAccount.getStatus() == Account.State.ONLINE) { - if (!mFetchingAvatar) { - mFetchingAvatar = true; - xmppConnectionService.checkForAvatar(mAccount, - mAvatarFetchCallback); - } - } else { - updateSaveButton(); - } - if (mAccount != null) { - updateAccountInformation(false); - } + public void refreshUiReal() { + invalidateOptionsMenu(); + if (mAccount != null + && mAccount.getStatus() != Account.State.ONLINE + && mFetchingAvatar) { + startActivity(new Intent(getApplicationContext(), + ManageAccountActivity.class)); + finish(); + } else if (jidToEdit == null && mAccount != null + && mAccount.getStatus() == Account.State.ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, + mAvatarFetchCallback); } - }); + } else { + updateSaveButton(); + } + if (mAccount != null) { + updateAccountInformation(false); + } + } + + @Override + public void onAccountUpdate() { + refreshUi(); } private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { @@ -271,10 +300,17 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } protected boolean accountInfoEdited() { - return this.mAccount != null && (!this.mAccount.getJid().toBareJid().toString().equals( - this.mAccountJid.getText().toString()) - || !this.mAccount.getPassword().equals( - this.mPassword.getText().toString())); + if (this.mAccount == null) { + return false; + } + final String unmodified; + if (Config.DOMAIN_LOCK != null) { + unmodified = this.mAccount.getJid().getLocalpart(); + } else { + unmodified = this.mAccount.getJid().toBareJid().toString(); + } + return !unmodified.equals(this.mAccountJid.getText().toString()) || + !this.mAccount.getPassword().equals(this.mPassword.getText().toString()); } @Override @@ -292,6 +328,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate setContentView(R.layout.activity_edit_account); this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); this.mAccountJid.addTextChangedListener(this.mTextWatcher); + this.mAccountJidLabel = (TextView) findViewById(R.id.account_jid_label); + if (Config.DOMAIN_LOCK != null) { + this.mAccountJidLabel.setText(R.string.username); + this.mAccountJid.setHint(R.string.username_hint); + } this.mPassword = (EditText) findViewById(R.id.account_password); this.mPassword.addTextChangedListener(this.mTextWatcher); this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm); @@ -307,9 +348,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking); this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); + this.mServerInfoHttpUpload = (TextView) findViewById(R.id.server_info_http_upload); this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint); + this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box); + this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard); + this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key); + this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card); + this.keys = (LinearLayout) findViewById(R.id.other_device_keys); this.mSaveButton = (Button) findViewById(R.id.save_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); @@ -328,6 +376,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } }; this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); + if (Config.DISALLOW_REGISTRATION_IN_UI) { + this.mRegisterNew.setVisibility(View.GONE); + } } @Override @@ -338,6 +389,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list); 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 clearDevices = menu.findItem(R.id.action_clear_devices); if (mAccount != null && mAccount.isOnlineAndConnected()) { if (!mAccount.getXmppConnection().getFeatures().blocking()) { showBlocklist.setVisible(false); @@ -345,11 +397,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if (!mAccount.getXmppConnection().getFeatures().register()) { changePassword.setVisible(false); } + Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); + if (otherDevices == null || otherDevices.isEmpty()) { + clearDevices.setVisible(false); + } } else { showQrCode.setVisible(false); showBlocklist.setVisible(false); showMoreInfo.setVisible(false); changePassword.setVisible(false); + clearDevices.setVisible(false); } return true; } @@ -363,6 +420,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } catch (final InvalidJidException | NullPointerException ignored) { this.jidToEdit = null; } + this.messageFingerprint = getIntent().getStringExtra("fingerprint"); if (this.jidToEdit != null) { this.mRegisterNew.setVisibility(View.GONE); if (getActionBar() != null) { @@ -379,9 +437,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate @Override protected void onBackendConnected() { - final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, - android.R.layout.simple_list_item_1, - xmppConnectionService.getKnownHosts()); if (this.jidToEdit != null) { this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); updateAccountInformation(true); @@ -394,7 +449,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mCancelButton.setEnabled(false); this.mCancelButton.setTextColor(getSecondaryTextColor()); } - this.mAccountJid.setAdapter(mKnownHostsAdapter); + if (Config.DOMAIN_LOCK == null) { + final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); + this.mAccountJid.setAdapter(mKnownHostsAdapter); + } updateSaveButton(); } @@ -415,13 +475,20 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate changePasswordIntent.putExtra("account", mAccount.getJid().toString()); startActivity(changePasswordIntent); break; + case R.id.action_clear_devices: + showWipePepDialog(); + break; } return super.onOptionsItemSelected(item); } private void updateAccountInformation(boolean init) { if (init) { - this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); + if (Config.DOMAIN_LOCK != null) { + this.mAccountJid.setText(this.mAccount.getJid().getLocalpart()); + } else { + this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); + } this.mPassword.setText(this.mAccount.getPassword()); } if (this.jidToEdit != null) { @@ -477,10 +544,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mServerInfoPep.setText(R.string.server_info_unavailable); } - final String fingerprint = this.mAccount.getOtrFingerprint(); - if (fingerprint != null) { + if (features.httpUpload()) { + this.mServerInfoHttpUpload.setText(R.string.server_info_available); + } else { + this.mServerInfoHttpUpload.setText(R.string.server_info_unavailable); + } + final String otrFingerprint = this.mAccount.getOtrFingerprint(); + if (otrFingerprint != null) { this.mOtrFingerprintBox.setVisibility(View.VISIBLE); - this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); + this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); this.mOtrFingerprintToClipboardButton .setVisibility(View.VISIBLE); this.mOtrFingerprintToClipboardButton @@ -489,7 +561,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate @Override public void onClick(final View v) { - if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) { + if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) { Toast.makeText( EditAccountActivity.this, R.string.toast_message_otr_fingerprint, @@ -500,6 +572,57 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mOtrFingerprintBox.setVisibility(View.GONE); } + final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint(); + if (axolotlFingerprint != null) { + this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE); + this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint)); + this.mAxolotlFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mAxolotlFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + + if (copyTextToClipboard(axolotlFingerprint, R.string.omemo_fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_omemo_fingerprint, + Toast.LENGTH_SHORT).show(); + } + } + }); + if (Config.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON) { + this.mRegenerateAxolotlKeyButton + .setVisibility(View.VISIBLE); + this.mRegenerateAxolotlKeyButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + showRegenerateAxolotlKeyDialog(); + } + }); + } + } else { + this.mAxolotlFingerprintBox.setVisibility(View.GONE); + } + final IdentityKey ownKey = mAccount.getAxolotlService().getOwnPublicKey(); + boolean hasKeys = false; + keys.removeAllViews(); + for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( + mAccount, mAccount.getJid().toBareJid().toString())) { + if(ownKey.equals(identityKey)) { + continue; + } + boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint); + hasKeys |= addFingerprintRow(keys, mAccount, identityKey, highlight); + } + if (hasKeys) { + keysCard.setVisibility(View.VISIBLE); + } else { + keysCard.setVisibility(View.GONE); + } } else { if (this.mAccount.errorStatus()) { this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); @@ -512,4 +635,41 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mStats.setVisibility(View.GONE); } } + + public void showRegenerateAxolotlKeyDialog() { + Builder builder = new Builder(this); + builder.setTitle("Regenerate Key"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAccount.getAxolotlService().regenerateKeys(); + } + }); + builder.create().show(); + } + + public void showWipePepDialog() { + Builder builder = new Builder(this); + builder.setTitle(getString(R.string.clear_other_devices)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.clear_other_devices_desc)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.accept), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAccount.getAxolotlService().wipeOtherPepDevices(); + } + }); + builder.create().show(); + } + + @Override + public void onKeyStatusUpdated() { + refreshUi(); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 56dbc55e..5b9b7355 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -1,27 +1,29 @@ package eu.siacs.conversations.ui; -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; -import eu.siacs.conversations.ui.adapter.AccountAdapter; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.os.Bundle; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ContextMenu.ContextMenuInfo; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.AccountAdapter; + public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate { protected Account selectedAccount = null; @@ -80,6 +82,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); } else { menu.findItem(R.id.mgmt_account_enable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(!Config.HIDE_PGP_IN_UI); } menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString()); } diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 4333dbdb..e01490f9 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -13,6 +13,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.PhoneHelper; @@ -192,7 +193,13 @@ public class PublishProfilePictureActivity extends XmppActivity { } else { loadImageIntoPreview(avatarUri); } - this.accountTextView.setText(this.account.getJid().toBareJid().toString()); + String account; + if (Config.DOMAIN_LOCK != null) { + account = this.account.getJid().getLocalpart(); + } else { + account = this.account.getJid().toBareJid().toString(); + } + this.accountTextView.setText(account); } } @@ -251,4 +258,8 @@ public class PublishProfilePictureActivity extends XmppActivity { this.publishButton.setTextColor(getSecondaryTextColor()); } + public void refreshUiReal() { + //nothing to do. This Activity doesn't implement any listeners + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index df2933c9..f9238f13 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -142,13 +142,14 @@ public class SettingsActivity extends XmppActivity implements .toLowerCase(Locale.US); if (xmppConnectionServiceBound) { for (Account account : xmppConnectionService.getAccounts()) { - account.setResource(resource); - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.resetStreamId(); - } - xmppConnectionService.reconnectAccountInBackground(account); + if (account.setResource(resource)) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.resetStreamId(); + } + xmppConnectionService.reconnectAccountInBackground(account); + } } } } @@ -192,4 +193,8 @@ public class SettingsActivity extends XmppActivity implements } } + public void refreshUiReal() { + //nothing to do. This Activity doesn't implement any listeners + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 351f1dfc..e0ebd5c2 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; @@ -229,4 +228,8 @@ public class ShareWithActivity extends XmppActivity { } + public void refreshUiReal() { + //nothing to do. This Activity doesn't implement any listeners + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 68e77af4..74621fc4 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -289,7 +289,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU protected void toggleContactBlock() { final int position = contact_context_id; - BlockContactDialog.show(this, xmppConnectionService, (Contact)contacts.get(position)); + BlockContactDialog.show(this, xmppConnectionService, (Contact) contacts.get(position)); } protected void deleteContact() { @@ -299,7 +299,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.action_delete_contact); builder.setMessage(getString(R.string.remove_contact_text, - contact.getJid())); + contact.getJid())); builder.setPositiveButton(R.string.delete, new OnClickListener() { @Override @@ -319,7 +319,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_bookmark); builder.setMessage(getString(R.string.remove_bookmark_text, - bookmark.getJid())); + bookmark.getJid())); builder.setPositiveButton(R.string.delete, new OnClickListener() { @Override @@ -368,7 +368,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } final Jid accountJid; try { - accountJid = Jid.fromString((String) spinner.getSelectedItem()); + if (Config.DOMAIN_LOCK != null) { + accountJid = Jid.fromParts((String) spinner.getSelectedItem(),Config.DOMAIN_LOCK,null); + } else { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } } catch (final InvalidJidException e) { return; } @@ -379,8 +383,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU jid.setError(getString(R.string.invalid_jid)); return; } - final Account account = xmppConnectionService - .findAccountByJid(accountJid); + final Account account = xmppConnectionService.findAccountByJid(accountJid); if (account == null) { dialog.dismiss(); return; @@ -428,7 +431,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } final Jid accountJid; try { - accountJid = Jid.fromString((String) spinner.getSelectedItem()); + if (Config.DOMAIN_LOCK != null) { + accountJid = Jid.fromParts((String) spinner.getSelectedItem(),Config.DOMAIN_LOCK,null); + } else { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } } catch (final InvalidJidException e) { return; } @@ -576,7 +583,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU this.mActivatedAccounts.clear(); for (Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { - this.mActivatedAccounts.add(account.getJid().toBareJid().toString()); + if (Config.DOMAIN_LOCK != null) { + this.mActivatedAccounts.add(account.getJid().getLocalpart()); + } else { + this.mActivatedAccounts.add(account.getJid().toBareJid().toString()); + } } } final Intent intent = getIntent(); diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java new file mode 100644 index 00000000..0e685c3e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -0,0 +1,265 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.whispersystems.libaxolotl.IdentityKey; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +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.xmpp.OnKeyStatusUpdated; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated { + private Jid accountJid; + private Jid contactJid; + private boolean hasOtherTrustedKeys = false; + private boolean hasPendingFetches = false; + private boolean hasNoTrustedKeys = true; + + private Contact contact; + private TextView keyErrorMessage; + private LinearLayout keyErrorMessageCard; + private TextView ownKeysTitle; + private LinearLayout ownKeys; + private LinearLayout ownKeysCard; + private TextView foreignKeysTitle; + private LinearLayout foreignKeys; + private LinearLayout foreignKeysCard; + private Button mSaveButton; + private Button mCancelButton; + + private final Map<IdentityKey, Boolean> ownKeysToTrust = new HashMap<>(); + private final Map<IdentityKey, Boolean> foreignKeysToTrust = new HashMap<>(); + + private final OnClickListener mSaveButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + commitTrusts(); + Intent data = new Intent(); + data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID)); + setResult(RESULT_OK, data); + finish(); + } + }; + + private final OnClickListener mCancelButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }; + + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + populateView(); + } + + @Override + protected String getShareableUri() { + if (contact != null) { + return contact.getShareableUri(); + } else { + return ""; + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_trust_keys); + try { + this.accountJid = Jid.fromString(getIntent().getExtras().getString("account")); + } catch (final InvalidJidException ignored) { + } + try { + this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact")); + } catch (final InvalidJidException ignored) { + } + hasNoTrustedKeys = getIntent().getBooleanExtra("has_no_trusted", false); + + keyErrorMessageCard = (LinearLayout) findViewById(R.id.key_error_message_card); + keyErrorMessage = (TextView) findViewById(R.id.key_error_message); + ownKeysTitle = (TextView) findViewById(R.id.own_keys_title); + ownKeys = (LinearLayout) findViewById(R.id.own_keys_details); + ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card); + foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title); + foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details); + foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card); + mCancelButton = (Button) findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(mCancelButtonListener); + mSaveButton = (Button) findViewById(R.id.save_button); + mSaveButton.setOnClickListener(mSaveButtonListener); + + + if (getActionBar() != null) { + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + private void populateView() { + setTitle(getString(R.string.trust_omemo_fingerprints)); + ownKeys.removeAllViews(); + foreignKeys.removeAllViews(); + boolean hasOwnKeys = false; + boolean hasForeignKeys = false; + for(final IdentityKey identityKey : ownKeysToTrust.keySet()) { + hasOwnKeys = true; + addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, false, + XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ownKeysToTrust.put(identityKey, isChecked); + // own fingerprints have no impact on locked status. + } + }, + null + ); + } + for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) { + hasForeignKeys = true; + addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, false, + XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + foreignKeysToTrust.put(identityKey, isChecked); + lockOrUnlockAsNeeded(); + } + }, + null + ); + } + + if(hasOwnKeys) { + ownKeysTitle.setText(accountJid.toString()); + ownKeysCard.setVisibility(View.VISIBLE); + } + if(hasForeignKeys) { + foreignKeysTitle.setText(contactJid.toString()); + foreignKeysCard.setVisibility(View.VISIBLE); + } + if(hasPendingFetches) { + setFetching(); + lock(); + } else { + if (!hasForeignKeys && !hasOtherTrustedKeys) { + keyErrorMessageCard.setVisibility(View.VISIBLE); + keyErrorMessage.setText(R.string.error_no_keys_to_trust); + ownKeys.removeAllViews(); ownKeysCard.setVisibility(View.GONE); + foreignKeys.removeAllViews(); foreignKeysCard.setVisibility(View.GONE); + } + lockOrUnlockAsNeeded(); + setDone(); + } + } + + private void getFingerprints(final Account account) { + Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED); + Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact); + if (hasNoTrustedKeys) { + ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED)); + foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact)); + } + for(final IdentityKey identityKey : ownKeysSet) { + if(!ownKeysToTrust.containsKey(identityKey)) { + ownKeysToTrust.put(identityKey, false); + } + } + for(final IdentityKey identityKey : foreignKeysSet) { + if(!foreignKeysToTrust.containsKey(identityKey)) { + foreignKeysToTrust.put(identityKey, false); + } + } + } + + @Override + public void onBackendConnected() { + if ((accountJid != null) && (contactJid != null)) { + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + ownKeysToTrust.clear(); + foreignKeysToTrust.clear(); + getFingerprints(account); + + if(account.getAxolotlService().getNumTrustedKeys(contact) > 0) { + hasOtherTrustedKeys = true; + } + Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false); + if(account.getAxolotlService().hasPendingKeyFetches(conversation)) { + hasPendingFetches = true; + } + + populateView(); + } + } + + @Override + public void onKeyStatusUpdated() { + final Account account = xmppConnectionService.findAccountByJid(accountJid); + hasPendingFetches = false; + getFingerprints(account); + refreshUi(); + } + + private void commitTrusts() { + for(IdentityKey identityKey:ownKeysToTrust.keySet()) { + contact.getAccount().getAxolotlService().setFingerprintTrust( + identityKey.getFingerprint().replaceAll("\\s", ""), + XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey))); + } + for(IdentityKey identityKey:foreignKeysToTrust.keySet()) { + contact.getAccount().getAxolotlService().setFingerprintTrust( + identityKey.getFingerprint().replaceAll("\\s", ""), + XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey))); + } + } + + private void unlock() { + mSaveButton.setEnabled(true); + mSaveButton.setTextColor(getPrimaryTextColor()); + } + + private void lock() { + mSaveButton.setEnabled(false); + mSaveButton.setTextColor(getSecondaryTextColor()); + } + + private void lockOrUnlockAsNeeded() { + if (!hasOtherTrustedKeys && !foreignKeysToTrust.values().contains(true)){ + lock(); + } else { + unlock(); + } + } + + private void setDone() { + mSaveButton.setText(getString(R.string.done)); + } + + private void setFetching() { + mSaveButton.setText(getString(R.string.fetching_keys)); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4157035b..2d780eda 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -43,8 +43,11 @@ import android.util.Log; 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; @@ -56,6 +59,8 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import net.java.otr4j.session.SessionID; +import org.whispersystems.libaxolotl.IdentityKey; + import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -66,6 +71,7 @@ import java.util.concurrent.RejectedExecutionException; import de.tzur.conversations.Settings; 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; @@ -75,7 +81,10 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; 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.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -91,6 +100,7 @@ public abstract class XmppActivity extends Activity { protected int mPrimaryTextColor; protected int mSecondaryTextColor; + protected int mTertiaryTextColor; protected int mPrimaryBackgroundColor; protected int mSecondaryBackgroundColor; protected int mColorRed; @@ -117,7 +127,7 @@ public abstract class XmppActivity extends Activity { protected ConferenceInvite mPendingConferenceInvite = null; - protected void refreshUi() { + protected final void refreshUi() { final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; if (diff > Config.REFRESH_UI_INTERVAL) { mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); @@ -129,9 +139,7 @@ public abstract class XmppActivity extends Activity { } } - protected void refreshUiReal() { - - }; + abstract protected void refreshUiReal(); protected interface OnValueEdited { public void onValueEdited(String value); @@ -288,6 +296,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnShowErrorToast) { this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); } + if (this instanceof OnKeyStatusUpdated) { + this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this); + } } protected void unregisterListeners() { @@ -309,6 +320,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnShowErrorToast) { this.xmppConnectionService.removeOnShowErrorToastListener(); } + if (this instanceof OnKeyStatusUpdated) { + this.xmppConnectionService.removeOnNewKeysAvailableListener(); + } } @Override @@ -337,7 +351,8 @@ public abstract class XmppActivity extends Activity { ExceptionHelper.init(getApplicationContext()); mPrimaryTextColor = getResources().getColor(R.color.black87); mSecondaryTextColor = getResources().getColor(R.color.black54); - mColorRed = getResources().getColor(R.color.red500); + mTertiaryTextColor = getResources().getColor(R.color.black12); + mColorRed = getResources().getColor(R.color.red800); mColorOrange = getResources().getColor(R.color.orange500); mColorGreen = getResources().getColor(R.color.green500); mPrimaryColor = getResources().getColor(R.color.green500); @@ -375,14 +390,18 @@ public abstract class XmppActivity extends Activity { public void switchToConversation(Conversation conversation, String text, boolean newTask) { - switchToConversation(conversation,text,null,newTask); + switchToConversation(conversation,text,null,false,newTask); } public void highlightInMuc(Conversation conversation, String nick) { - switchToConversation(conversation, null, nick, false); + switchToConversation(conversation, null, nick, false, false); + } + + public void privateMsgInMuc(Conversation conversation, String nick) { + switchToConversation(conversation, null, nick, true, false); } - private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) { + private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) { Intent viewConversationIntent = new Intent(this, ConversationActivity.class); viewConversationIntent.setAction(Intent.ACTION_VIEW); @@ -393,6 +412,7 @@ public abstract class XmppActivity extends Activity { } if (nick != null) { viewConversationIntent.putExtra(ConversationActivity.NICK, nick); + viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm); } viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); if (newTask) { @@ -408,10 +428,15 @@ public abstract class XmppActivity extends Activity { } public void switchToContactDetails(Contact contact) { + switchToContactDetails(contact, null); + } + + public void switchToContactDetails(Contact contact, String messageFingerprint) { Intent intent = new Intent(this, ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString()); intent.putExtra("contact", contact.getJid().toString()); + intent.putExtra("fingerprint", messageFingerprint); startActivity(intent); } @@ -592,6 +617,124 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } + protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey, boolean highlight) { + final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + final XmppAxolotlSession.Trust trust = account.getAxolotlService() + .getFingerprintTrust(fingerprint); + return addFingerprintRowWithListeners(keys, account, identityKey, 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); + } + } + + ); + } + + protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account, + final IdentityKey identityKey, + boolean highlight, + XmppAxolotlSession.Trust trust, + boolean showTag, + CompoundButton.OnCheckedChangeListener + onCheckedChangeListener, + View.OnClickListener onClickListener) { + 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); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust); + trustToggle.setVisibility(View.VISIBLE); + trustToggle.setOnCheckedChangeListener(onCheckedChangeListener); + trustToggle.setOnClickListener(onClickListener); + view.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + showPurgeKeyDialog(account, identityKey); + return true; + } + }); + + switch (trust) { + case UNTRUSTED: + case TRUSTED: + trustToggle.setChecked(trust == XmppAxolotlSession.Trust.TRUSTED, false); + trustToggle.setEnabled(true); + 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()); + break; + case INACTIVE_TRUSTED: + trustToggle.setOnClickListener(null); + trustToggle.setChecked(true, false); + trustToggle.setEnabled(false); + key.setTextColor(getTertiaryTextColor()); + keyType.setTextColor(getTertiaryTextColor()); + break; + } + + if (showTag) { + keyType.setText(getString(R.string.omemo_fingerprint)); + } else { + keyType.setVisibility(View.GONE); + } + if (highlight) { + keyType.setTextColor(getResources().getColor(R.color.accent)); + keyType.setText(getString(R.string.omemo_fingerprint_selected_message)); + } else { + keyType.setText(getString(R.string.omemo_fingerprint)); + } + + key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); + keys.addView(view); + return true; + } + + public void showPurgeKeyDialog(final Account account, final IdentityKey identityKey) { + 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(identityKey.getFingerprint()) + + "\n\n" + getString(R.string.purge_key_desc_part2)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.accept), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + account.getAxolotlService().purgeKey(identityKey); + refreshUi(); + } + }); + builder.create().show(); + } + public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { final Contact contact = conversation.getContact(); @@ -711,6 +854,10 @@ public abstract class XmppActivity extends Activity { } }; + public int getTertiaryTextColor() { + return this.mTertiaryTextColor; + } + public int getSecondaryTextColor() { return this.mSecondaryTextColor; } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 782a1231..141dbc3a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -1,11 +1,5 @@ package eu.siacs.conversations.ui.adapter; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.ManageAccountActivity; import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -16,6 +10,15 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Switch; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.ui.widget.Switch; + public class AccountAdapter extends ArrayAdapter<Account> { private XmppActivity activity; @@ -34,7 +37,11 @@ public class AccountAdapter extends ArrayAdapter<Account> { view = inflater.inflate(R.layout.account_row, parent, false); } TextView jid = (TextView) view.findViewById(R.id.account_jid); - jid.setText(account.getJid().toBareJid().toString()); + if (Config.DOMAIN_LOCK != null) { + jid.setText(account.getJid().getLocalpart()); + } else { + jid.setText(account.getJid().toBareJid().toString()); + } TextView statusView = (TextView) view.findViewById(R.id.account_status); ImageView imageView = (ImageView) view.findViewById(R.id.account_image); imageView.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48))); @@ -53,8 +60,7 @@ public class AccountAdapter extends ArrayAdapter<Account> { } final Switch tglAccountState = (Switch) view.findViewById(R.id.tgl_account_status); final boolean isDisabled = (account.getStatus() == Account.State.DISABLED); - tglAccountState.setOnCheckedChangeListener(null); - tglAccountState.setChecked(!isDisabled); + tglAccountState.setChecked(!isDisabled,false); tglAccountState.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 7443c0cb..b89dd01a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -24,9 +24,8 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.utils.UIHelper; @@ -119,8 +118,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { Pair<String,Boolean> preview = UIHelper.getMessagePreview(activity,message); mLastMessage.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); - CharSequence msgText = preview.first; - mLastMessage.setText(msgText); + mLastMessage.setText(preview.first); if (preview.second) { if (conversation.isRead()) { mLastMessage.setTypeface(null, Typeface.ITALIC); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java index 0993735f..471526af 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java @@ -1,13 +1,13 @@ package eu.siacs.conversations.ui.adapter; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - import android.content.Context; import android.widget.ArrayAdapter; import android.widget.Filter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + public class KnownHostsAdapter extends ArrayAdapter<String> { private ArrayList<String> domains; private Filter domainFilter = new Filter() { 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 b6979cc6..a5b25967 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.preference.PreferenceManager; @@ -29,13 +30,14 @@ import java.util.List; import de.tzur.conversations.Settings; 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.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; @@ -83,18 +85,30 @@ public class MessageAdapter extends ArrayAdapter<Message> { return 3; } - @Override - public int getItemViewType(int position) { - if (getItem(position).getType() == Message.TYPE_STATUS) { + public int getItemViewType(Message message) { + if (message.getType() == Message.TYPE_STATUS) { return STATUS; - } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) { + } else if (message.getStatus() <= Message.STATUS_RECEIVED) { return RECEIVED; + } + + return SENT; + } + + @Override + public int getItemViewType(int position) { + return this.getItemViewType(getItem(position)); + } + + private int getMessageTextColor(int type, boolean primary) { + if (type == SENT) { + return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54); } else { - return SENT; + return activity.getResources().getColor(primary ? R.color.white : R.color.white70); } } - private void displayStatus(ViewHolder viewHolder, Message message) { + private void displayStatus(ViewHolder viewHolder, Message message, int type) { String filesize = null; String info = null; boolean error = false; @@ -149,15 +163,39 @@ public class MessageAdapter extends ArrayAdapter<Message> { } break; } - if (error) { + if (error && type == SENT) { viewHolder.time.setTextColor(activity.getWarningTextColor()); } else { - viewHolder.time.setTextColor(activity.getSecondaryTextColor()); + viewHolder.time.setTextColor(this.getMessageTextColor(type,false)); } if (message.getEncryption() == Message.ENCRYPTION_NONE) { viewHolder.indicator.setVisibility(View.GONE); } else { viewHolder.indicator.setVisibility(View.VISIBLE); + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + XmppAxolotlSession.Trust trust = message.getConversation() + .getAccount().getAxolotlService().getFingerprintTrust( + message.getAxolotlFingerprint()); + + if(trust == null || trust != XmppAxolotlSession.Trust.TRUSTED) { + viewHolder.indicator.setColorFilter(activity.getWarningTextColor()); + viewHolder.indicator.setAlpha(1.0f); + } else { + viewHolder.indicator.clearColorFilter(); + if (type == SENT) { + viewHolder.indicator.setAlpha(0.57f); + } else { + viewHolder.indicator.setAlpha(0.7f); + } + } + } else { + viewHolder.indicator.clearColorFilter(); + if (type == SENT) { + viewHolder.indicator.setAlpha(0.57f); + } else { + viewHolder.indicator.setAlpha(0.7f); + } + } } String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), @@ -189,19 +227,19 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayInfoMessage(ViewHolder viewHolder, String text) { + private void displayInfoMessage(ViewHolder viewHolder, String text, int type) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(text); - viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); + viewHolder.messageBody.setTextColor(getMessageTextColor(type,false)); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); viewHolder.messageBody.setTextIsSelectable(false); } - private void displayDecryptionFailed(ViewHolder viewHolder) { + private void displayDecryptionFailed(ViewHolder viewHolder, int type) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } @@ -209,7 +247,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(getContext().getString( R.string.decryption_failed)); - viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); + viewHolder.messageBody.setTextColor(getMessageTextColor(type,false)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(false); } @@ -227,7 +265,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setText(span); } - private void displayTextMessage(final ViewHolder viewHolder, final Message message) { + private void displayTextMessage(final ViewHolder viewHolder, final Message message, int type) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } @@ -270,8 +308,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } final Spannable span = new SpannableString(privateMarker + " " + formattedBody); - span.setSpan(new ForegroundColorSpan(activity - .getSecondaryTextColor()), 0, privateMarker + span.setSpan(new ForegroundColorSpan(getMessageTextColor(type,false)), 0, privateMarker .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); span.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), @@ -286,7 +323,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else { viewHolder.messageBody.setText(""); } - viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor()); + viewHolder.messageBody.setTextColor(this.getMessageTextColor(type,true)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(true); } @@ -355,17 +392,15 @@ public class MessageAdapter extends ArrayAdapter<Message> { scalledW = (int) target; scalledH = (int) (params.height / ((double) params.width / target)); } - viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( - scalledW, scalledH)); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scalledW, scalledH); + layoutParams.setMargins(0, (int)(metrics.density * 4), 0, (int)(metrics.density * 4)); + viewHolder.image.setLayoutParams(layoutParams); activity.loadBitmap(message, viewHolder.image); viewHolder.image.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(activity.xmppConnectionService - .getFileBackend().getJingleFileUri(message), "image/*"); - getContext().startActivity(intent); + openDownloadable(message); } }); viewHolder.image.setOnLongClickListener(openContextMenu); @@ -494,7 +529,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); } else { - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first); + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,type); } } else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { displayImageMessage(viewHolder, message); @@ -506,10 +541,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { if (activity.hasPgp()) { - displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message)); + displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message),type); } else { - displayInfoMessage(viewHolder, - activity.getString(R.string.install_openkeychain)); + displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),type); if (viewHolder != null) { viewHolder.message_box .setOnClickListener(new OnClickListener() { @@ -522,18 +556,26 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayDecryptionFailed(viewHolder); + displayDecryptionFailed(viewHolder,type); } else { if (GeoHelper.isGeoUri(message.getBody())) { displayLocationMessage(viewHolder,message); } else if (message.treatAsDownloadable() == Message.Decision.MUST) { displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); } else { - displayTextMessage(viewHolder, message); + displayTextMessage(viewHolder, message, type); + } + } + + if (type == RECEIVED) { + if(message.isValidInSession()) { + viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received); + } else { + viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); } } - displayStatus(viewHolder, message); + displayStatus(viewHolder, message, type); return view; } @@ -546,7 +588,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { Toast.LENGTH_SHORT).show(); } } else if (message.treatAsDownloadable() != Message.Decision.NEVER) { - activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message); + activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message,true); } } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Switch.java b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java new file mode 100644 index 00000000..fd3b5553 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java @@ -0,0 +1,68 @@ +package eu.siacs.conversations.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.kyleduo.switchbutton.SwitchButton; + +public class Switch extends SwitchButton { + + private int mTouchSlop; + private int mClickTimeout; + private float mStartX; + private float mStartY; + private OnClickListener mOnClickListener; + + public Switch(Context context) { + super(context); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); + } + + public Switch(Context context, AttributeSet attrs) { + super(context, attrs); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); + } + + public Switch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); + } + + @Override + public void setOnClickListener(OnClickListener onClickListener) { + this.mOnClickListener = onClickListener; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) { + float deltaX = event.getX() - mStartX; + float deltaY = event.getY() - mStartY; + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mStartX = event.getX(); + mStartY = event.getY(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + float time = event.getEventTime() - event.getDownTime(); + if (deltaX < mTouchSlop && deltaY < mTouchSlop && time < mClickTimeout) { + if (mOnClickListener != null) { + this.mOnClickListener.onClick(this); + } + } + break; + default: + break; + } + return true; + } + return super.onTouchEvent(event); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 2dec203d..c7c9ac42 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -96,11 +96,10 @@ public final class CryptoHelper { } else if (fingerprint.length() < 40) { return fingerprint; } - StringBuilder builder = new StringBuilder(fingerprint); - builder.insert(8, " "); - builder.insert(17, " "); - builder.insert(26, " "); - builder.insert(35, " "); + StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s","")); + for(int i=8;i<builder.length();i+=9) { + builder.insert(i, ' '); + } return builder.toString(); } diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 5a47bb3c..4d0dd3da 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -1,17 +1,7 @@ package eu.siacs.conversations.utils; -import de.measite.minidns.Client; -import de.measite.minidns.DNSMessage; -import de.measite.minidns.Record; -import de.measite.minidns.Record.TYPE; -import de.measite.minidns.Record.CLASS; -import de.measite.minidns.record.SRV; -import de.measite.minidns.record.A; -import de.measite.minidns.record.AAAA; -import de.measite.minidns.record.Data; -import de.measite.minidns.util.NameUtil; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.xmpp.jid.Jid; +import android.os.Bundle; +import android.util.Log; import java.io.IOException; import java.net.InetAddress; @@ -22,8 +12,18 @@ import java.util.Random; import java.util.TreeMap; import java.util.regex.Pattern; -import android.os.Bundle; -import android.util.Log; +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Record; +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.SRV; +import de.measite.minidns.util.NameUtil; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.jid.Jid; public class DNSHelper { @@ -38,17 +38,14 @@ public class DNSHelper { public static Bundle getSRVRecord(final Jid jid) throws IOException { final String host = jid.getDomainpart(); String dns[] = client.findDNS(); - - if (dns != null) { - for (String dnsserver : dns) { - InetAddress ip = InetAddress.getByName(dnsserver); - Bundle b = queryDNS(host, ip); - if (b.containsKey("values")) { - return b; - } + for (int i = 0; i < dns.length; ++i) { + InetAddress ip = InetAddress.getByName(dns[i]); + Bundle b = queryDNS(host, ip); + if (b.containsKey("values") || i == dns.length - 1) { + return b; } } - return queryDNS(host, InetAddress.getByName("8.8.8.8")); + return null; } public static Bundle queryDNS(String host, InetAddress dnsServer) { @@ -132,7 +129,6 @@ public class DNSHelper { } catch (SocketTimeoutException e) { bundle.putString("error", "timeout"); } catch (Exception e) { - e.printStackTrace(); bundle.putString("error", "unhandled"); } return bundle; diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java index 0ad57fe2..4e3ec236 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import android.content.Context; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; @@ -8,8 +10,6 @@ import java.io.StringWriter; import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; -import android.content.Context; - public class ExceptionHandler implements UncaughtExceptionHandler { private UncaughtExceptionHandler defaultHandler; diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index ee3ea3e1..0f182847 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -1,5 +1,17 @@ package eu.siacs.conversations.utils; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceManager; +import android.text.format.DateUtils; +import android.util.Log; + import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; @@ -15,18 +27,6 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.content.DialogInterface.OnClickListener; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.preference.PreferenceManager; -import android.text.format.DateUtils; -import android.util.Log; - public class ExceptionHelper { public static void init(Context context) { if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java new file mode 100644 index 00000000..a13300d4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java @@ -0,0 +1,144 @@ +package eu.siacs.conversations.utils; + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +public class FileUtils { + + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @author paulburke + */ + @SuppressLint("NewApi") + public static String getPath(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index a9e89d1b..d4544424 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -458,7 +458,7 @@ public final class MimeUtils { if (extension == null || extension.isEmpty()) { return null; } - return extensionToMimeTypeMap.get(extension); + return extensionToMimeTypeMap.get(extension.toLowerCase()); } /** * Returns true if the given extension has a registered MIME type. diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java index 9a689768..f18a4ed8 100644 --- a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java +++ b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -1,9 +1,9 @@ package eu.siacs.conversations.utils; -import java.util.List; - import android.os.Bundle; +import java.util.List; + public interface OnPhoneContactsLoadedListener { public void onPhoneContactsLoaded(List<Bundle> phoneContacts); } diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index 99e8ebb8..a37f60a0 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -1,9 +1,5 @@ package eu.siacs.conversations.utils; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - import android.content.Context; import android.content.CursorLoader; import android.content.Loader; @@ -15,6 +11,9 @@ import android.os.Bundle; import android.provider.ContactsContract; import android.provider.ContactsContract.Profile; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + public class PhoneHelper { public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) { @@ -98,7 +97,7 @@ public class PhoneHelper { if (packageName != null) { try { return context.getPackageManager().getPackageInfo(packageName, 0).versionName; - } catch (final PackageManager.NameNotFoundException e) { + } catch (final PackageManager.NameNotFoundException | RuntimeException e) { return "unknown"; } } else { diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 76f48037..44a38728 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -1,17 +1,15 @@ package eu.siacs.conversations.utils; +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.util.Pair; + import java.util.ArrayList; import java.util.Arrays; -import java.net.URLConnection; import java.util.Calendar; import java.util.Date; -import java.util.HashMap; -import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Pattern; -import java.util.regex.Matcher; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -22,14 +20,6 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.xmpp.jid.Jid; -import android.content.Context; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.Spannable.Factory; -import android.text.style.ImageSpan; -import android.text.Spannable; -import android.util.Pair; - public class UIHelper { private static String BLACK_HEART_SUIT = "\u2665"; diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 32657c66..7b4937b2 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -21,6 +21,11 @@ public class Element { this.name = name; } + public Element(String name, String xmlns) { + this.name = name; + this.setAttribute("xmlns", xmlns); + } + public Element addChild(Element child) { this.content = null; children.add(child); @@ -64,10 +69,9 @@ public class Element { public Element findChild(String name, String xmlns) { for (Element child : this.children) { - if (child.getName().equals(name) - && (child.getAttribute("xmlns").equals(xmlns))) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { return child; - } + } } return null; } diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index 52d3d46a..aeaaa593 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -1,18 +1,18 @@ package eu.siacs.conversations.xml; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import eu.siacs.conversations.Config; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.util.Log; -import android.util.Xml; +import eu.siacs.conversations.Config; public class XmlReader { private XmlPullParser parser; diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java new file mode 100644 index 00000000..65ae133d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp; + +public interface OnKeyStatusUpdated { + public void onKeyStatusUpdated(); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 35c89b45..604325be 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1,13 +1,10 @@ package eu.siacs.conversations.xmpp; -import android.content.Context; -import android.content.SharedPreferences; import android.os.Bundle; import android.os.Parcelable; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; -import android.preference.PreferenceManager; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -26,7 +23,6 @@ import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.SocketAddress; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; @@ -35,9 +31,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import javax.net.ssl.HostnameVerifier; @@ -81,21 +77,20 @@ public class XmppConnection implements Runnable { private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; - private final Context applicationContext; protected Account account; private final WakeLock wakeLock; private Socket socket; private XmlReader tagReader; private TagWriter tagWriter; private final Features features = new Features(this); - private boolean shouldBind = true; + private boolean needsBinding = true; private boolean shouldAuthenticate = true; private Element streamFeatures; private final HashMap<Jid, Info> disco = new HashMap<>(); private String streamId = null; private int smVersion = 3; - private final SparseArray<String> messageReceipts = new SparseArray<>(); + private final SparseArray<String> mStanzaReceipts = new SparseArray<>(); private int stanzasReceived = 0; private int stanzasSent = 0; @@ -104,7 +99,7 @@ public class XmppConnection implements Runnable { private long lastConnect = 0; private long lastSessionStarted = 0; private int attempt = 0; - private final Map<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>(); + private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>(); private OnPresencePacketReceived presenceListener = null; private OnJinglePacketReceived jingleListener = null; private OnIqPacketReceived unregisteredIqListener = null; @@ -123,7 +118,6 @@ public class XmppConnection implements Runnable { PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); tagWriter = new TagWriter(); mXmppConnectionService = service; - applicationContext = service.getApplicationContext(); } protected void changeStatus(final Account.State nextStatus) { @@ -151,10 +145,9 @@ public class XmppConnection implements Runnable { lastPingSent = SystemClock.elapsedRealtime(); this.attempt++; try { - shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER); + shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER); tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); - packetCallbacks.clear(); this.changeStatus(Account.State.CONNECTING); if (DNSHelper.isIp(account.getServer().toString())) { socket = new Socket(); @@ -165,6 +158,9 @@ public class XmppConnection implements Runnable { } } else { final Bundle result = DNSHelper.getSRVRecord(account.getServer()); + if (result == null) { + throw new IOException("unhandled exception in DNS resolver"); + } final ArrayList<Parcelable> values = result.getParcelableArrayList("values"); if ("timeout".equals(result.getString("error"))) { throw new IOException("timeout in dns"); @@ -198,10 +194,7 @@ public class XmppConnection implements Runnable { socket = new Socket(); socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); socketError = false; - } catch (final UnknownHostException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - i++; - } catch (final IOException e) { + } catch (final Throwable e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; } @@ -338,23 +331,24 @@ public class XmppConnection implements Runnable { + ": session resumed with lost packages"); stanzasSent = serverCount; } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": session resumed"); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed"); } - if (acknowledgedListener != null) { - for (int i = 0; i < messageReceipts.size(); ++i) { - if (serverCount >= messageReceipts.keyAt(i)) { - acknowledgedListener.onMessageAcknowledged( - account, messageReceipts.valueAt(i)); - } + acknowledgeStanzaUpTo(serverCount); + ArrayList<IqPacket> failedIqPackets = new ArrayList<>(); + for(int i = 0; i < this.mStanzaReceipts.size(); ++i) { + String id = mStanzaReceipts.valueAt(i); + Pair<IqPacket,OnIqPacketReceived> pair = id == null ? null : this.packetCallbacks.get(id); + if (pair != null) { + failedIqPackets.add(pair.first); } } - messageReceipts.clear(); + mStanzaReceipts.clear(); + Log.d(Config.LOGTAG,"resending "+failedIqPackets.size()+" iq stanza"); + for(IqPacket packet : failedIqPackets) { + sendUnmodifiedIqPacket(packet,null); + } } catch (final NumberFormatException ignored) { } - sendServiceDiscoveryInfo(account.getServer()); - sendServiceDiscoveryInfo(account.getJid().toBareJid()); - sendServiceDiscoveryItems(account.getServer()); sendInitialPing(); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); @@ -368,17 +362,7 @@ public class XmppConnection implements Runnable { lastPacketReceived = SystemClock.elapsedRealtime(); try { final int serverSequence = Integer.parseInt(ack.getAttribute("h")); - if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + serverSequence); - } - final String msgId = this.messageReceipts.get(serverSequence); - if (msgId != null) { - if (this.acknowledgedListener != null) { - this.acknowledgedListener.onMessageAcknowledged( - account, msgId); - } - this.messageReceipts.remove(serverSequence); - } + acknowledgeStanzaUpTo(serverSequence); } catch (NumberFormatException e) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number"); } @@ -406,6 +390,22 @@ public class XmppConnection implements Runnable { } } + private void acknowledgeStanzaUpTo(int serverCount) { + for (int i = 0; i < mStanzaReceipts.size(); ++i) { + if (serverCount >= mStanzaReceipts.keyAt(i)) { + if (Config.EXTENDED_SM_LOGGING) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaReceipts.keyAt(i)); + } + String id = mStanzaReceipts.valueAt(i); + if (acknowledgedListener != null) { + acknowledgedListener.onMessageAcknowledged(account, id); + } + mStanzaReceipts.removeAt(i); + i--; + } + } + } + private void sendInitialPing() { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); @@ -414,9 +414,12 @@ public class XmppConnection implements Runnable { this.sendIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": online with resource " + account.getResource()); - changeStatus(Account.State.ONLINE); + if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource()); + changeStatus(Account.State.ONLINE); + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": initial ping failed"); + } } }); } @@ -481,26 +484,28 @@ public class XmppConnection implements Runnable { this.jingleListener.onJinglePacketReceived(account,(JinglePacket) packet); } } else { - if (packetCallbacks.containsKey(packet.getId())) { - final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId()); - // Packets to the server should have responses from the server - if (packetCallbackDuple.first.toServer(account)) { - if (packet.fromServer(account)) { - packetCallbackDuple.second.onIqPacketReceived(account, packet); - packetCallbacks.remove(packet.getId()); - } else { - Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); - } - } else { - if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) { - packetCallbackDuple.second.onIqPacketReceived(account, packet); - packetCallbacks.remove(packet.getId()); + synchronized (this.packetCallbacks) { + if (packetCallbacks.containsKey(packet.getId())) { + final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId()); + // Packets to the server should have responses from the server + if (packetCallbackDuple.first.toServer(account)) { + if (packet.fromServer(account)) { + packetCallbackDuple.second.onIqPacketReceived(account, packet); + packetCallbacks.remove(packet.getId()); + } else { + Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet"); + } } else { - Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet"); + if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) { + packetCallbackDuple.second.onIqPacketReceived(account, packet); + packetCallbacks.remove(packet.getId()); + } else { + Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet"); + } } + } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); } - } else if (packet.getType() == IqPacket.TYPE.GET|| packet.getType() == IqPacket.TYPE.SET) { - this.unregisteredIqListener.onIqPacketReceived(account, packet); } } } @@ -521,14 +526,6 @@ public class XmppConnection implements Runnable { tagWriter.writeTag(startTLS); } - private SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(applicationContext); - } - - private boolean enableLegacySSL() { - return getPreferences().getBoolean("enable_legacy_ssl", false); - } - private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException { tagReader.readTag(); try { @@ -629,19 +626,18 @@ public class XmppConnection implements Runnable { } else { throw new IncompatibleServerException(); } - } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" - + smVersion) - && streamId != null) { + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) { if (Config.EXTENDED_SM_LOGGING) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived); } final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); this.tagWriter.writeStanzaAsync(resume); - } else if (this.streamFeatures.hasChild("bind") && shouldBind) { - sendBindRequest(); - } else { - disconnect(true); - changeStatus(Account.State.INCOMPATIBLE_SERVER); + } else if (needsBinding) { + if (this.streamFeatures.hasChild("bind")) { + sendBindRequest(); + } else { + throw new IncompatibleServerException(); + } } } @@ -662,8 +658,8 @@ public class XmppConnection implements Runnable { @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { - final Element instructions = packet.query().findChild("instructions"); - if (packet.query().hasChild("username") + if (packet.getType() == IqPacket.TYPE.RESULT + && packet.query().hasChild("username") && (packet.query().hasChild("password"))) { final IqPacket register = new IqPacket(IqPacket.TYPE.SET); final Element username = new Element("username").setContent(account.getUsername()); @@ -690,6 +686,7 @@ public class XmppConnection implements Runnable { } }); } else { + final Element instructions = packet.query().findChild("instructions"); changeStatus(Account.State.REGISTRATION_FAILED); disconnect(true); Log.d(Config.LOGTAG, account.getJid().toBareJid() @@ -707,6 +704,8 @@ public class XmppConnection implements Runnable { } catch (final InterruptedException ignored) { } } + needsBinding = false; + clearIqCallbacks(); final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") .addChild("resource").setContent(account.getResource()); @@ -728,24 +727,48 @@ public class XmppConnection implements Runnable { sendPostBindInitialization(); } } else { + Log.d(Config.LOGTAG,account.getJid()+": disconnecting because of bind failure"); disconnect(true); } } else { + Log.d(Config.LOGTAG,account.getJid()+": disconnecting because of bind failure"); disconnect(true); } } }); } + private void clearIqCallbacks() { + final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT); + final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>(); + synchronized (this.packetCallbacks) { + if (this.packetCallbacks.size() == 0) { + return; + } + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks"); + final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator(); + while (iterator.hasNext()) { + Pair<IqPacket, OnIqPacketReceived> entry = iterator.next(); + callbacks.add(entry.second); + iterator.remove(); + } + } + for(OnIqPacketReceived callback : callbacks) { + callback.onIqPacketReceived(account,failurePacket); + } + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done clearing iq callbacks. "+this.packetCallbacks.size()+" left"); + } + private void sendStartSession() { final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); - startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); + startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE.RESULT) { sendPostBindInitialization(); } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions"); disconnect(true); } } @@ -763,7 +786,7 @@ public class XmppConnection implements Runnable { final EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; - messageReceipts.clear(); + mStanzaReceipts.clear(); } features.carbonsEnabled = false; features.blockListRequested = false; @@ -790,26 +813,29 @@ public class XmppConnection implements Runnable { @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { - final List<Element> elements = packet.query().getChildren(); - final Info info = new Info(); - for (final Element element : elements) { - if (element.getName().equals("identity")) { - String type = element.getAttribute("type"); - String category = element.getAttribute("category"); - if (type != null && category != null) { - info.identities.add(new Pair<>(category,type)); + if (packet.getType() == IqPacket.TYPE.RESULT) { + final List<Element> elements = packet.query().getChildren(); + final Info info = new Info(); + for (final Element element : elements) { + if (element.getName().equals("identity")) { + String type = element.getAttribute("type"); + String category = element.getAttribute("category"); + if (type != null && category != null) { + info.identities.add(new Pair<>(category, type)); + } + } else if (element.getName().equals("feature")) { + info.features.add(element.getAttribute("var")); } - } else if (element.getName().equals("feature")) { - info.features.add(element.getAttribute("var")); } - } - disco.put(jid, info); - - if (account.getServer().equals(jid)) { - enableAdvancedStreamFeatures(); - for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { - listener.onAdvancedStreamFeaturesAvailable(account); + disco.put(jid, info); + if (account.getServer().equals(jid)) { + enableAdvancedStreamFeatures(); + for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { + listener.onAdvancedStreamFeaturesAvailable(account); + } } + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not query disco info for "+jid.toString()); } } }); @@ -834,14 +860,18 @@ public class XmppConnection implements Runnable { @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { - final List<Element> elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = element.getAttributeAsJid("jid"); - if (jid != null && !jid.equals(account.getServer())) { - sendServiceDiscoveryInfo(jid); + if (packet.getType() == IqPacket.TYPE.RESULT) { + final List<Element> elements = packet.query().getChildren(); + for (final Element element : elements) { + if (element.getName().equals("item")) { + final Jid jid = element.getAttributeAsJid("jid"); + if (jid != null && !jid.equals(account.getServer())) { + sendServiceDiscoveryInfo(jid); + } } } + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not query disco items of "+server); } } }); @@ -875,6 +905,8 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": switching resource due to conflict (" + account.getResource() + ")"); + } else if (streamError != null) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString()); } } @@ -895,7 +927,7 @@ public class XmppConnection implements Runnable { public void sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { packet.setFrom(account.getJid()); - this.sendUnmodifiedIqPacket(packet,callback); + this.sendUnmodifiedIqPacket(packet, callback); } @@ -905,10 +937,9 @@ public class XmppConnection implements Runnable { packet.setAttribute("id", id); } if (callback != null) { - if (packet.getId() == null) { - packet.setId(nextRandomId()); + synchronized (this.packetCallbacks) { + packetCallbacks.put(packet.getId(), new Pair<>(packet, callback)); } - packetCallbacks.put(packet.getId(), new Pair<>(packet, callback)); } this.sendPacket(packet); } @@ -932,11 +963,11 @@ public class XmppConnection implements Runnable { ++stanzasSent; } tagWriter.writeStanzaAsync(packet); - if (packet instanceof MessagePacket && packet.getId() != null && getFeatures().sm()) { + if ((packet instanceof MessagePacket || packet instanceof IqPacket) && packet.getId() != null && this.streamId != null) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for stanza #" + stanzasSent); } - this.messageReceipts.put(stanzasSent, packet.getId()); + this.mStanzaReceipts.put(stanzasSent, packet.getId()); tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); } } @@ -1005,7 +1036,7 @@ public class XmppConnection implements Runnable { if (tagWriter.isActive()) { tagWriter.finish(); try { - while (!tagWriter.finished()) { + while (!tagWriter.finished() && socket.isConnected()) { Log.d(Config.LOGTAG, "not yet finished"); Thread.sleep(100); } 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 2c56488d..42b14330 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -1,5 +1,13 @@ package eu.siacs.conversations.xmpp.jingle; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.util.Pair; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -8,17 +16,18 @@ import java.util.Locale; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import android.content.Intent; -import android.net.Uri; -import android.os.SystemClock; -import android.util.Log; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.entities.TransferablePlaceholder; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; @@ -59,20 +68,24 @@ public class JingleConnection implements Transferable { private String contentCreator; private int mProgress = 0; - private long mLastGuiRefresh = 0; private boolean receivedCandidate = false; private boolean sentCandidate = false; private boolean acceptedAutomatically = false; + private XmppAxolotlMessage mXmppAxolotlMessage; + private JingleTransport transport = null; + private OutputStream mFileOutputStream; + private InputStream mFileInputStream; + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() != IqPacket.TYPE.RESULT) { fail(); } } @@ -84,15 +97,13 @@ public class JingleConnection implements Transferable { public void onFileTransmitted(DownloadableFile file) { if (responder.equals(account.getJid())) { sendSuccess(); + mXmppConnectionService.getFileBackend().updateFileParams(message); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED); if (acceptedAutomatically) { message.markUnread(); - JingleConnection.this.mXmppConnectionService - .getNotificationService().push(message); + JingleConnection.this.mXmppConnectionService.getNotificationService().push(message); } - mXmppConnectionService.getFileBackend().updateFileParams(message); - mXmppConnectionService.databaseBackend.createMessage(message); - mXmppConnectionService.markMessage(message, - Message.STATUS_RECEIVED); } else { if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { file.delete(); @@ -113,6 +124,14 @@ public class JingleConnection implements Transferable { } }; + public InputStream getFileInputStream() { + return this.mFileInputStream; + } + + public OutputStream getFileOutputStream() { + return this.mFileOutputStream; + } + private OnProxyActivated onProxyActivated = new OnProxyActivated() { @Override @@ -194,7 +213,22 @@ public class JingleConnection implements Transferable { mXmppConnectionService.sendIqPacket(account,response,null); } - public void init(Message message) { + public void init(final Message message) { + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + Conversation conversation = message.getConversation(); + conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() { + @Override + public void run(XmppAxolotlMessage xmppAxolotlMessage) { + init(message, xmppAxolotlMessage); + } + }); + } else { + init(message, null); + } + } + + private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) { + this.mXmppAxolotlMessage = xmppAxolotlMessage; this.contentCreator = "initiator"; this.contentName = this.mJingleConnectionManager.nextRandomId(); this.message = message; @@ -238,8 +272,7 @@ public class JingleConnection implements Transferable { }); mergeCandidate(candidate); } else { - Log.d(Config.LOGTAG, - "no primary candidate of our own was found"); + Log.d(Config.LOGTAG, "no primary candidate of our own was found"); sendInitRequest(); } } @@ -267,13 +300,16 @@ public class JingleConnection implements Transferable { this.contentCreator = content.getAttribute("creator"); this.contentName = content.getAttribute("name"); this.transportId = content.getTransportId(); - this.mergeCandidates(JingleCandidate.parse(content.socks5transport() - .getChildren())); + this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); this.fileOffer = packet.getJingleContent().getFileOffer(); mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null); if (fileOffer != null) { + Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX); + if (encrypted != null) { + this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid()); + } Element fileSize = fileOffer.findChild("size"); Element fileNameElement = fileOffer.findChild("name"); if (fileNameElement != null) { @@ -333,22 +369,36 @@ public class JingleConnection implements Transferable { + " allowed size:" + this.mJingleConnectionManager .getAutoAcceptFileSize()); - this.mXmppConnectionService.getNotificationService() - .push(message); + this.mXmppConnectionService.getNotificationService().push(message); } - this.file = this.mXmppConnectionService.getFileBackend() - .getFile(message, false); - if (message.getEncryption() == Message.ENCRYPTION_OTR) { + this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + if (mXmppAxolotlMessage != null) { + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage); + if (transportMessage != null) { + message.setEncryption(Message.ENCRYPTION_AXOLOTL); + this.file.setKey(transportMessage.getKey()); + this.file.setIv(transportMessage.getIv()); + message.setAxolotlFingerprint(transportMessage.getFingerprint()); + } else { + Log.d(Config.LOGTAG,"could not process KeyTransportMessage"); + } + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { byte[] key = conversation.getSymmetricKey(); if (key == null) { this.sendCancel(); this.fail(); return; } else { - this.file.setKey(key); + this.file.setKeyAndIv(key); } } - this.file.setExpectedSize(size); + this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL); + if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) { + this.file.setExpectedSize((size / 16 + 1) * 16); + } else { + this.file.setExpectedSize(size); + } + Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); } else { this.sendCancel(); this.fail(); @@ -364,19 +414,35 @@ public class JingleConnection implements Transferable { Content content = new Content(this.contentCreator, this.contentName); if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend().getFile( - message, false); - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - Conversation conversation = this.message.getConversation(); - if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key"); - cancel(); + this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + Pair<InputStream,Integer> pair; + try { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key"); + cancel(); + } + this.file.setKeyAndIv(conversation.getSymmetricKey()); + pair = AbstractConnectionManager.createInputStream(this.file, false); + this.file.setExpectedSize(pair.second); + content.setFileOffer(this.file, true); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + this.file.setKey(mXmppAxolotlMessage.getInnerKey()); + this.file.setIv(mXmppAxolotlMessage.getIV()); + pair = AbstractConnectionManager.createInputStream(this.file, true); + this.file.setExpectedSize(pair.second); + content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement()); + } else { + pair = AbstractConnectionManager.createInputStream(this.file, false); + this.file.setExpectedSize(pair.second); + content.setFileOffer(this.file, false); } - content.setFileOffer(this.file, true); - this.file.setKey(conversation.getSymmetricKey()); - } else { - content.setFileOffer(this.file, false); + } catch (FileNotFoundException e) { + cancel(); + return; } + this.mFileInputStream = pair.first; this.transportId = this.mJingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); content.socks5transport().setChildren(getCandidatesAsElements()); @@ -385,7 +451,7 @@ public class JingleConnection implements Transferable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { + if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer"); mJingleStatus = JINGLE_STATUS_INITIATED; mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); @@ -570,12 +636,11 @@ public class JingleConnection implements Transferable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() != IqPacket.TYPE.RESULT) { onProxyActivated.failed(); } else { onProxyActivated.success(); - sendProxyActivated(connection - .getCandidate().getCid()); + sendProxyActivated(connection.getCandidate().getCid()); } } }); @@ -748,6 +813,8 @@ public class JingleConnection implements Transferable { if (this.transport != null && this.transport instanceof JingleInbandTransport) { this.transport.disconnect(); } + FileBackend.close(mFileInputStream); + FileBackend.close(mFileOutputStream); if (this.message != null) { if (this.responder.equals(account.getJid())) { this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED)); @@ -901,10 +968,7 @@ public class JingleConnection implements Transferable { public void updateProgress(int i) { this.mProgress = i; - if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { - this.mLastGuiRefresh = SystemClock.elapsedRealtime(); - mXmppConnectionService.updateConversationUi(); - } + mXmppConnectionService.updateConversationUi(); } interface OnProxyActivated { @@ -956,4 +1020,8 @@ public class JingleConnection implements Transferable { public int getProgress() { return this.mProgress; } + + public AbstractConnectionManager getConnectionManager() { + return this.mJingleConnectionManager; + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index cadf9df3..2c888d0f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,16 +1,19 @@ package eu.siacs.conversations.xmpp.jingle; +import android.annotation.SuppressLint; +import android.util.Log; + import java.math.BigInteger; import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import android.annotation.SuppressLint; -import android.util.Log; + import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Xmlns; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 9a02ee7a..85280c5c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Base64; +import android.util.Log; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -7,9 +10,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import android.util.Base64; -import android.util.Log; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; @@ -74,7 +74,7 @@ public class JingleInbandTransport extends JingleTransport { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() != IqPacket.TYPE.RESULT) { callback.failed(); } else { callback.established(); @@ -93,7 +93,7 @@ public class JingleInbandTransport extends JingleTransport { digest.reset(); file.getParentFile().mkdirs(); file.createNewFile(); - this.fileOutputStream = file.createOutputStream(); + this.fileOutputStream = connection.getFileOutputStream(); if (this.fileOutputStream == null) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream"); callback.onFileTransferAborted(); @@ -112,15 +112,11 @@ public class JingleInbandTransport extends JingleTransport { this.onFileTransmissionStatusChanged = callback; this.file = file; try { - if (this.file.getKey() != null) { - this.remainingSize = (this.file.getSize() / 16 + 1) * 16; - } else { - this.remainingSize = this.file.getSize(); - } + this.remainingSize = this.file.getExpectedSize(); this.fileSize = this.remainingSize; this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); - fileInputStream = this.file.createInputStream(); + fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream"); callback.onFileTransferAborted(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 8d74f44e..556395ae 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import android.os.PowerManager; import android.util.Log; import java.io.FileNotFoundException; @@ -96,23 +97,24 @@ public class JingleSocks5Transport extends JingleTransport { } - public void send(final DownloadableFile file, - final OnFileTransmissionStatusChanged callback) { + public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { new Thread(new Runnable() { @Override public void run() { InputStream fileInputStream = null; + final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_"+connection.getSessionId()); try { + wakeLock.acquire(); MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); - fileInputStream = file.createInputStream(); + fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream"); callback.onFileTransferAborted(); return; } - long size = file.getSize(); + long size = file.getExpectedSize(); long transmitted = 0; int count; byte[] buffer = new byte[8192]; @@ -138,6 +140,7 @@ public class JingleSocks5Transport extends JingleTransport { callback.onFileTransferAborted(); } finally { FileBackend.close(fileInputStream); + wakeLock.release(); } } }).start(); @@ -150,14 +153,16 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { OutputStream fileOutputStream = null; + final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_"+connection.getSessionId()); try { + wakeLock.acquire(); MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); inputStream.skip(45); socket.setSoTimeout(30000); file.getParentFile().mkdirs(); file.createNewFile(); - fileOutputStream = file.createOutputStream(); + fileOutputStream = connection.getFileOutputStream(); if (fileOutputStream == null) { callback.onFileTransferAborted(); Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream"); @@ -166,7 +171,7 @@ public class JingleSocks5Transport extends JingleTransport { double size = file.getExpectedSize(); long remainingSize = file.getExpectedSize(); byte[] buffer = new byte[8192]; - int count = buffer.length; + int count; while (remainingSize > 0) { count = inputStream.read(buffer); if (count == -1) { @@ -194,7 +199,9 @@ public class JingleSocks5Transport extends JingleTransport { Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { + wakeLock.release(); FileBackend.close(fileOutputStream); + FileBackend.close(inputStream); } } }).start(); @@ -209,27 +216,9 @@ public class JingleSocks5Transport extends JingleTransport { } public void disconnect() { - if (this.outputStream != null) { - try { - this.outputStream.close(); - } catch (IOException e) { - - } - } - if (this.inputStream != null) { - try { - this.inputStream.close(); - } catch (IOException e) { - - } - } - if (this.socket != null) { - try { - this.socket.close(); - } catch (IOException e) { - - } - } + FileBackend.close(inputStream); + FileBackend.close(outputStream); + FileBackend.close(socket); } public boolean isEstablished() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java index e832d3f5..b3211158 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -1,5 +1,31 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.DownloadableFile; public abstract class JingleTransport { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index bcadbe77..f752cc5d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -25,17 +25,18 @@ public class Content extends Element { this.transportId = sid; } - public void setFileOffer(DownloadableFile actualFile, boolean otr) { + public Element setFileOffer(DownloadableFile actualFile, boolean otr) { Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); Element offer = description.addChild("offer"); Element file = offer.addChild("file"); - file.addChild("size").setContent(Long.toString(actualFile.getSize())); + file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize())); if (otr) { file.addChild("name").setContent(actualFile.getName() + ".otr"); } else { file.addChild("name").setContent(actualFile.getName()); } + return file; } public Element getFileOffer() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 74da6a9b..38bb5c8f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -1,10 +1,10 @@ package eu.siacs.conversations.xmpp.pep; +import android.util.Base64; + import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.Jid; -import android.util.Base64; - public class Avatar { public enum Origin { PEP, VCARD }; diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java index 7b36fc49..49b27408 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -4,12 +4,13 @@ import eu.siacs.conversations.xml.Element; public class IqPacket extends AbstractStanza { - public static enum TYPE { + public enum TYPE { ERROR, SET, RESULT, GET, - INVALID + INVALID, + TIMEOUT } public IqPacket(final TYPE type) { @@ -39,6 +40,9 @@ public class IqPacket extends AbstractStanza { public TYPE getType() { final String type = getAttribute("type"); + if (type == null) { + return TYPE.INVALID; + } switch (type) { case "error": return TYPE.ERROR; diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index e32811af..6b690912 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.stanzas; import android.util.Pair; -import java.text.ParseException; - import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.xml.Element; @@ -29,6 +27,11 @@ public class MessagePacket extends AbstractStanza { this.children.add(0, body); } + public void setAxolotlMessage(Element axolotlMessage) { + this.children.remove(findChild("body")); + this.children.add(0, axolotlMessage); + } + public void setType(int type) { switch (type) { case TYPE_CHAT: diff --git a/src/main/res/drawable-hdpi/ic_action_done.png b/src/main/res/drawable-hdpi/ic_action_done.png Binary files differnew file mode 100644 index 00000000..58bf9721 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_done.png diff --git a/src/main/res/drawable-hdpi/ic_done_black_24dp.png b/src/main/res/drawable-hdpi/ic_done_black_24dp.png Binary files differnew file mode 100644 index 00000000..d4c06072 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_done_black_24dp.png diff --git a/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 00000000..51cc4dbd --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/res/drawable-hdpi/ic_secure_indicator.png b/src/main/res/drawable-hdpi/ic_secure_indicator.png Binary files differindex 2a2934fb..220463fc 100644 --- a/src/main/res/drawable-hdpi/ic_secure_indicator.png +++ b/src/main/res/drawable-hdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-hdpi/ic_secure_indicator_white.png b/src/main/res/drawable-hdpi/ic_secure_indicator_white.png Binary files differnew file mode 100644 index 00000000..46eb1195 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_secure_indicator_white.png diff --git a/src/main/res/drawable-hdpi/message_bubble_received.9.png b/src/main/res/drawable-hdpi/message_bubble_received.9.png Binary files differnew file mode 100644 index 00000000..1a496771 --- /dev/null +++ b/src/main/res/drawable-hdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 00000000..9cd2f825 --- /dev/null +++ b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-hdpi/message_bubble_sent.9.png b/src/main/res/drawable-hdpi/message_bubble_sent.9.png Binary files differnew file mode 100644 index 00000000..2bb4e04f --- /dev/null +++ b/src/main/res/drawable-hdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-hdpi/switch_thumb_disable.png b/src/main/res/drawable-hdpi/switch_thumb_disable.png Binary files differnew file mode 100644 index 00000000..1e9b151b --- /dev/null +++ b/src/main/res/drawable-hdpi/switch_thumb_disable.png diff --git a/src/main/res/drawable-hdpi/switch_thumb_off_normal.png b/src/main/res/drawable-hdpi/switch_thumb_off_normal.png Binary files differnew file mode 100644 index 00000000..b7c1fc11 --- /dev/null +++ b/src/main/res/drawable-hdpi/switch_thumb_off_normal.png diff --git a/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png Binary files differnew file mode 100644 index 00000000..ca6e4909 --- /dev/null +++ b/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png diff --git a/src/main/res/drawable-hdpi/switch_thumb_on_normal.png b/src/main/res/drawable-hdpi/switch_thumb_on_normal.png Binary files differnew file mode 100644 index 00000000..cbcda5d4 --- /dev/null +++ b/src/main/res/drawable-hdpi/switch_thumb_on_normal.png diff --git a/src/main/res/drawable-hdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-hdpi/switch_thumb_on_pressed.png Binary files differnew file mode 100644 index 00000000..234b12dc --- /dev/null +++ b/src/main/res/drawable-hdpi/switch_thumb_on_pressed.png diff --git a/src/main/res/drawable-mdpi/ic_action_done.png b/src/main/res/drawable-mdpi/ic_action_done.png Binary files differnew file mode 100644 index 00000000..cf5fab3a --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_done.png diff --git a/src/main/res/drawable-mdpi/ic_done_black_24dp.png b/src/main/res/drawable-mdpi/ic_done_black_24dp.png Binary files differnew file mode 100644 index 00000000..5e5e7cf2 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_done_black_24dp.png diff --git a/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 00000000..c136c59f --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/res/drawable-mdpi/ic_secure_indicator.png b/src/main/res/drawable-mdpi/ic_secure_indicator.png Binary files differindex 5a73aef4..690d4d03 100644 --- a/src/main/res/drawable-mdpi/ic_secure_indicator.png +++ b/src/main/res/drawable-mdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-mdpi/ic_secure_indicator_white.png b/src/main/res/drawable-mdpi/ic_secure_indicator_white.png Binary files differnew file mode 100644 index 00000000..e2f894ef --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_secure_indicator_white.png diff --git a/src/main/res/drawable-mdpi/message_bubble_received.9.png b/src/main/res/drawable-mdpi/message_bubble_received.9.png Binary files differnew file mode 100644 index 00000000..7e134402 --- /dev/null +++ b/src/main/res/drawable-mdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 00000000..38f75e05 --- /dev/null +++ b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-mdpi/message_bubble_sent.9.png b/src/main/res/drawable-mdpi/message_bubble_sent.9.png Binary files differnew file mode 100644 index 00000000..b42664a8 --- /dev/null +++ b/src/main/res/drawable-mdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-mdpi/switch_thumb_disable.png b/src/main/res/drawable-mdpi/switch_thumb_disable.png Binary files differnew file mode 100644 index 00000000..968de345 --- /dev/null +++ b/src/main/res/drawable-mdpi/switch_thumb_disable.png diff --git a/src/main/res/drawable-mdpi/switch_thumb_off_normal.png b/src/main/res/drawable-mdpi/switch_thumb_off_normal.png Binary files differnew file mode 100644 index 00000000..51fb4d7a --- /dev/null +++ b/src/main/res/drawable-mdpi/switch_thumb_off_normal.png diff --git a/src/main/res/drawable-mdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-mdpi/switch_thumb_off_pressed.png Binary files differnew file mode 100644 index 00000000..ca788445 --- /dev/null +++ b/src/main/res/drawable-mdpi/switch_thumb_off_pressed.png diff --git a/src/main/res/drawable-mdpi/switch_thumb_on_normal.png b/src/main/res/drawable-mdpi/switch_thumb_on_normal.png Binary files differnew file mode 100644 index 00000000..6a93d5f7 --- /dev/null +++ b/src/main/res/drawable-mdpi/switch_thumb_on_normal.png diff --git a/src/main/res/drawable-mdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-mdpi/switch_thumb_on_pressed.png Binary files differnew file mode 100644 index 00000000..e8d7bd0f --- /dev/null +++ b/src/main/res/drawable-mdpi/switch_thumb_on_pressed.png diff --git a/src/main/res/drawable-xhdpi/ic_action_done.png b/src/main/res/drawable-xhdpi/ic_action_done.png Binary files differnew file mode 100644 index 00000000..b8915716 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_done.png diff --git a/src/main/res/drawable-xhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xhdpi/ic_done_black_24dp.png Binary files differnew file mode 100644 index 00000000..64a4944f --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_done_black_24dp.png diff --git a/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 00000000..7891efff --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/res/drawable-xhdpi/ic_secure_indicator.png b/src/main/res/drawable-xhdpi/ic_secure_indicator.png Binary files differindex 1f4c9a32..cd0d1391 100644 --- a/src/main/res/drawable-xhdpi/ic_secure_indicator.png +++ b/src/main/res/drawable-xhdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-xhdpi/ic_secure_indicator_white.png b/src/main/res/drawable-xhdpi/ic_secure_indicator_white.png Binary files differnew file mode 100644 index 00000000..b624a8ce --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_secure_indicator_white.png diff --git a/src/main/res/drawable-xhdpi/message_bubble_received.9.png b/src/main/res/drawable-xhdpi/message_bubble_received.9.png Binary files differnew file mode 100644 index 00000000..dc8ee534 --- /dev/null +++ b/src/main/res/drawable-xhdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 00000000..b5be808f --- /dev/null +++ b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png Binary files differnew file mode 100644 index 00000000..f1c46232 --- /dev/null +++ b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-xhdpi/switch_thumb_disable.png b/src/main/res/drawable-xhdpi/switch_thumb_disable.png Binary files differnew file mode 100644 index 00000000..7f677324 --- /dev/null +++ b/src/main/res/drawable-xhdpi/switch_thumb_disable.png diff --git a/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png Binary files differnew file mode 100644 index 00000000..4199d322 --- /dev/null +++ b/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png diff --git a/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png Binary files differnew file mode 100644 index 00000000..2b86888f --- /dev/null +++ b/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png diff --git a/src/main/res/drawable-xhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xhdpi/switch_thumb_on_normal.png Binary files differnew file mode 100644 index 00000000..daa30990 --- /dev/null +++ b/src/main/res/drawable-xhdpi/switch_thumb_on_normal.png diff --git a/src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png Binary files differnew file mode 100644 index 00000000..6aab47c9 --- /dev/null +++ b/src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png diff --git a/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png Binary files differnew file mode 100644 index 00000000..c9c01741 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 00000000..9c1e27d7 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_secure_indicator.png b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png Binary files differindex 1ee9b67d..6a74ccbe 100644 --- a/src/main/res/drawable-xxhdpi/ic_secure_indicator.png +++ b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-xxhdpi/ic_secure_indicator_white.png b/src/main/res/drawable-xxhdpi/ic_secure_indicator_white.png Binary files differnew file mode 100644 index 00000000..4945c959 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_secure_indicator_white.png diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png Binary files differnew file mode 100644 index 00000000..6ce84487 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 00000000..a3181869 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png Binary files differnew file mode 100644 index 00000000..9f36a649 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_disable.png b/src/main/res/drawable-xxhdpi/switch_thumb_disable.png Binary files differnew file mode 100644 index 00000000..db7c1df4 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/switch_thumb_disable.png diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png Binary files differnew file mode 100644 index 00000000..f747b559 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png Binary files differnew file mode 100644 index 00000000..b9fe6d46 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png Binary files differnew file mode 100644 index 00000000..88199024 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png Binary files differnew file mode 100644 index 00000000..7a4fed54 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png diff --git a/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png Binary files differnew file mode 100644 index 00000000..2f6d6386 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 00000000..e44a6d28 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png Binary files differnew file mode 100644 index 00000000..e7fa6754 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png Binary files differnew file mode 100644 index 00000000..398e53a2 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png Binary files differnew file mode 100644 index 00000000..be428cd2 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png b/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png Binary files differnew file mode 100644 index 00000000..3970168c --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png Binary files differnew file mode 100644 index 00000000..ea8d4f89 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png Binary files differnew file mode 100644 index 00000000..84d667b1 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png Binary files differnew file mode 100644 index 00000000..06b190eb --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png Binary files differnew file mode 100644 index 00000000..79c30d1e --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png diff --git a/src/main/res/drawable/message_border.xml b/src/main/res/drawable/account_image_border.xml index 990d0288..990d0288 100644 --- a/src/main/res/drawable/message_border.xml +++ b/src/main/res/drawable/account_image_border.xml diff --git a/src/main/res/drawable/switch_back_off.xml b/src/main/res/drawable/switch_back_off.xml new file mode 100644 index 00000000..20d2fb14 --- /dev/null +++ b/src/main/res/drawable/switch_back_off.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_enabled="false"><shape android:shape="rectangle"> + <solid android:color="#D5D5D5" /> + + <corners android:radius="99dp" /> + </shape></item> + <item android:state_enabled="true"><shape android:shape="rectangle"> + <solid android:color="#939393" /> + + <corners android:radius="99dp" /> + </shape></item> + +</selector>
\ No newline at end of file diff --git a/src/main/res/drawable/switch_back_on.xml b/src/main/res/drawable/switch_back_on.xml new file mode 100644 index 00000000..45117a98 --- /dev/null +++ b/src/main/res/drawable/switch_back_on.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false"> + <shape android:shape="rectangle"> + <solid android:color="#D5D5D5"/> + <corners android:radius="99dp"/> + </shape> + </item> + <item android:state_enabled="true"> + <shape android:shape="rectangle"> + <!-- 30% accent on white --> + <solid android:color="#b3ddf7"/> + <corners android:radius="99dp"/> + </shape> + </item> +</selector>
\ No newline at end of file diff --git a/src/main/res/drawable/switch_thumb.xml b/src/main/res/drawable/switch_thumb.xml new file mode 100644 index 00000000..ba3d1c45 --- /dev/null +++ b/src/main/res/drawable/switch_thumb.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:drawable="@drawable/switch_thumb_disable" android:state_enabled="false"/> + <item android:drawable="@drawable/switch_thumb_on_pressed" android:state_checked="true" android:state_pressed="true"/> + <item android:drawable="@drawable/switch_thumb_on_pressed" android:state_checked="true" android:state_focused="true"/> + <item android:drawable="@drawable/switch_thumb_on_normal" android:state_checked="true"/> + <item android:drawable="@drawable/switch_thumb_off_pressed" android:state_checked="false" android:state_pressed="true"/> + <item android:drawable="@drawable/switch_thumb_off_pressed" android:state_checked="false" android:state_focused="true"/> + <item android:drawable="@drawable/switch_thumb_off_normal" android:state_checked="false"/> + +</selector>
\ No newline at end of file diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index 06716a10..1837475c 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/activatedBackgroundIndicator" diff --git a/src/main/res/layout/activity_about.xml b/src/main/res/layout/activity_about.xml index d7d23f0f..247e96e5 100644 --- a/src/main/res/layout/activity_about.xml +++ b/src/main/res/layout/activity_about.xml @@ -17,5 +17,6 @@ android:paddingBottom="@dimen/activity_vertical_margin" android:textColor="@color/black87" android:textSize="?attr/TextSizeBody" - android:typeface="monospace"/> + android:typeface="monospace" + android:fontFamily="monospace"/> </ScrollView> diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 98de84f5..9d7a670b 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/grey200" > + android:background="@color/grey200"> <ScrollView android:layout_width="fill_parent" @@ -39,6 +40,7 @@ android:orientation="vertical" android:layout_toRightOf="@+id/avater"> <TextView + android:id="@+id/account_jid_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_jabber_id" @@ -299,6 +301,26 @@ android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> + <TableRow + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/server_info_http_upload" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/server_info_http_upload" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody" + tools:ignore="RtlHardcoded"/> + </TableRow> </TableLayout> <RelativeLayout @@ -320,7 +342,8 @@ android:layout_height="wrap_content" android:textColor="@color/black87" android:textSize="?attr/TextSizeBody" - android:typeface="monospace" /> + android:typeface="monospace" + android:fontFamily="monospace"/> <TextView android:layout_width="wrap_content" @@ -342,6 +365,96 @@ android:visibility="visible" android:contentDescription="@string/copy_otr_clipboard_description"/> </RelativeLayout> + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:id="@+id/axolotl_fingerprint_box" + android:layout_marginTop="32dp"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/axolotl_actions" + android:orientation="vertical"> + + <TextView + android:id="@+id/axolotl_fingerprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" + android:fontFamily="monospace"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black54" + android:textSize="?attr/TextSizeInfo" + android:text="@string/this_device_omemo_fingerprint"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/axolotl_actions" + android:layout_centerVertical="true" + android:layout_alignParentRight="true" + android:orientation="vertical"> + + <ImageButton + android:id="@+id/action_copy_axolotl_to_clipboard" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?android:selectableItemBackground" + android:padding="@dimen/image_button_padding" + android:src="?attr/icon_copy" + android:visibility="visible" + android:contentDescription="@string/copy_omemo_clipboard_description"/> + <ImageButton + android:id="@+id/action_regenerate_axolotl_key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?android:selectableItemBackground" + android:padding="@dimen/image_button_padding" + android:src="?attr/icon_refresh" + android:visibility="gone" + android:contentDescription="@string/regenerate_omemo_key"/> + + </LinearLayout> + </RelativeLayout> + </LinearLayout> + <LinearLayout + android:id="@+id/other_device_keys_card" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="@dimen/infocard_padding" + android:visibility="gone"> + + <TextView + android:id="@+id/other_device_keys_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" + android:text="@string/other_devices"/> + + <LinearLayout + android:id="@+id/other_device_keys" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="?android:dividerHorizontal" + android:orientation="vertical" + android:showDividers="middle" > + </LinearLayout> </LinearLayout> </LinearLayout> </ScrollView> diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index b89c9945..8c75760a 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@color/grey200"> diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml index 4b249745..3b249a66 100644 --- a/src/main/res/layout/activity_publish_profile_picture.xml +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -12,7 +12,7 @@ android:layout_centerHorizontal="true" android:layout_marginBottom="8dp" android:layout_marginTop="24dp" - android:background="@drawable/message_border" > + android:background="@drawable/account_image_border" > <ImageView android:id="@+id/account_image" diff --git a/src/main/res/layout/activity_trust_keys.xml b/src/main/res/layout/activity_trust_keys.xml new file mode 100644 index 00000000..6f8a1bc9 --- /dev/null +++ b/src/main/res/layout/activity_trust_keys.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/grey200" > + <ScrollView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/button_bar" + android:layout_alignParentTop="true" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/key_error_message_card" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="@dimen/infocard_padding" + android:visibility="gone"> + + <TextView + android:id="@+id/key_error_message_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" + android:text="@string/error_trustkeys_title"/> + + <TextView + android:id="@+id/key_error_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody" + android:padding="8dp"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/own_keys_card" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="@dimen/infocard_padding" + android:visibility="gone"> + + <TextView + android:id="@+id/own_keys_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold"/> + + <LinearLayout + android:id="@+id/own_keys_details" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="?android:dividerHorizontal" + android:showDividers="middle" + android:orientation="vertical"> + </LinearLayout> + + </LinearLayout> + + <LinearLayout + android:id="@+id/foreign_keys_card" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="@dimen/infocard_padding" + android:visibility="gone"> + + <TextView + android:id="@+id/foreign_keys_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold"/> + + <LinearLayout + android:id="@+id/foreign_keys_details" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="?android:dividerHorizontal" + android:showDividers="middle" + android:orientation="vertical"> + </LinearLayout> + + </LinearLayout> + + </LinearLayout> + </ScrollView> + <LinearLayout + android:id="@+id/button_bar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" > + + <Button + android:id="@+id/cancel_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/cancel" + android:textColor="@color/black87" /> + + <View + android:layout_width="1dp" + android:layout_height="fill_parent" + android:layout_marginBottom="7dp" + android:layout_marginTop="7dp" + android:background="@color/black12" /> + + <Button + android:id="@+id/save_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:enabled="true" + android:textColor="@color/black54" + android:text="@string/done"/> + </LinearLayout> +</RelativeLayout> diff --git a/src/main/res/layout/activity_verify_otr.xml b/src/main/res/layout/activity_verify_otr.xml index ab21c693..c15f19d5 100644 --- a/src/main/res/layout/activity_verify_otr.xml +++ b/src/main/res/layout/activity_verify_otr.xml @@ -36,7 +36,8 @@ android:layout_height="wrap_content" android:textColor="@color/black87" android:textSize="?attr/TextSizeBody" - android:typeface="monospace"/> + android:typeface="monospace" + android:fontFamily="monospace"/> <TextView android:layout_width="wrap_content" @@ -52,7 +53,8 @@ android:layout_marginTop="20dp" android:textColor="@color/black87" android:textSize="?attr/TextSizeBody" - android:typeface="monospace"/> + android:typeface="monospace" + android:fontFamily="monospace"/> <TextView android:layout_width="wrap_content" diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml index 8a8e311b..29cd9886 100644 --- a/src/main/res/layout/contact.xml +++ b/src/main/res/layout/contact.xml @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?android:attr/activatedBackgroundIndicator" - android:padding="8dp" > + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/activatedBackgroundIndicator" + android:padding="8dp"> <ImageView android:id="@+id/contact_photo" @@ -12,14 +13,6 @@ android:layout_alignParentLeft="true" android:scaleType="centerCrop" android:src="@drawable/ic_profile" /> - <TextView - android:layout_width="48dp" - android:layout_height="4dp" - android:textAppearance="?android:attr/textAppearanceSmall" - android:id="@+id/contact_status" - android:layout_below="@+id/contact_photo" - android:paddingTop="8dp" - android:paddingRight="8dp"/> <LinearLayout android:layout_width="wrap_content" @@ -58,6 +51,7 @@ android:textColor="@color/black87" android:textSize="?attr/TextSizeHeadline" android:typeface="monospace" + android:fontFamily="monospace" android:visibility="gone" /> </LinearLayout> diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index 933b72b4..f0bd2fc1 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -3,39 +3,67 @@ android:layout_width="wrap_content" android:layout_height="match_parent" > - <LinearLayout + <RelativeLayout + android:id="@+id/key_data" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" - android:layout_toLeftOf="@+id/button_remove" - android:orientation="vertical" - android:padding="8dp" > + android:paddingTop="8dp" + android:paddingLeft="8dp" + android:paddingBottom="8dp"> <TextView android:id="@+id/key" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/black87" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/tgl_trust" android:textSize="?attr/TextSizeBody" - android:typeface="monospace" /> + android:typeface="monospace" + android:fontFamily="monospace"/> <TextView android:id="@+id/key_type" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/black54" + android:layout_alignParentLeft="true" + android:layout_below="@+id/key" + android:maxLines="1" + android:textSize="?attr/TextSizeInfo"/> + + <TextView + android:id="@+id/key_trust" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_below="@+id/key" + android:visibility="gone" + android:textColor="@color/black54" android:textSize="?attr/TextSizeInfo"/> - </LinearLayout> <ImageButton android:id="@+id/button_remove" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" + android:layout_toRightOf="@+id/key" android:layout_centerVertical="true" android:background="?android:selectableItemBackground" android:padding="@dimen/image_button_padding" android:src="?attr/icon_remove" - android:visibility="invisible" /> + android:visibility="gone" /> + + + <Switch + android:id="@+id/tgl_trust" + android:visibility="invisible" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + style="@style/MD"/> + </RelativeLayout> </RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/conversation_list_row.xml b/src/main/res/layout/conversation_list_row.xml index c79628d4..21b0f389 100644 --- a/src/main/res/layout/conversation_list_row.xml +++ b/src/main/res/layout/conversation_list_row.xml @@ -1,11 +1,8 @@ -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:emojicon="http://schemas.android.com/apk/res-auto" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:padding="8dp" - android:descendantFocusability="blocksDescendants"> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:descendantFocusability="blocksDescendants"> <View android:layout_width="fill_parent" @@ -32,15 +29,6 @@ android:layout_alignParentLeft="true" android:scaleType="centerCrop" /> - <TextView - android:layout_width="56dp" - android:layout_height="4dp" - android:textAppearance="?android:attr/textAppearanceSmall" - android:id="@+id/status" - android:layout_below="@+id/conversation_image" - android:paddingTop="8dp" - android:paddingRight="8dp"/> - <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content" @@ -48,7 +36,6 @@ android:layout_toRightOf="@+id/conversation_image" android:paddingLeft="8dp" > - <TextView android:id="@+id/conversation_name" android:layout_width="wrap_content" @@ -98,4 +85,4 @@ </RelativeLayout> </RelativeLayout> </FrameLayout> -</FrameLayout> +</FrameLayout>
\ No newline at end of file diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 321055e9..927e7239 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -25,7 +25,7 @@ android:listSelector="@android:color/transparent" android:stackFromBottom="true" android:transcriptMode="normal" - tools:listitem="@layout/message_sent" > + tools:listitem="@layout/message_sent"> </ListView> </android.support.v4.widget.SwipeRefreshLayout> diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml index 68ed5711..fefe6af1 100644 --- a/src/main/res/layout/message_received.xml +++ b/src/main/res/layout/message_received.xml @@ -1,14 +1,23 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:emojicon="http://schemas.android.com/apk/res-auto" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingBottom="4dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="4dp" > +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:emojicon="http://schemas.android.com/apk/res-auto" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="3dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="3dp"> + + <ImageView + android:id="@+id/message_photo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:scaleType="fitXY" + android:src="@drawable/ic_profile" /> <LinearLayout android:id="@+id/message_box" @@ -16,28 +25,27 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_toRightOf="@+id/message_photo" - android:background="@drawable/message_border" - android:minHeight="48dp" + android:background="@drawable/message_bubble_received" + android:minHeight="53dp" + android:layout_marginTop="-2dp" + android:layout_marginRight="-4dp" android:longClickable="true"> <LinearLayout android:layout_width="wrap_content" android:layout_height="fill_parent" - android:background="@color/grey50" android:gravity="center_vertical" android:orientation="vertical" - android:paddingBottom="4dp" - android:paddingLeft="5dp" - android:paddingRight="5dp" - android:paddingTop="4dp" > + android:padding="2dp"> <ImageView android:id="@+id/message_image" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="4dp" android:adjustViewBounds="true" android:background="@color/black87" - android:paddingBottom="2dp" android:scaleType="centerCrop" /> <github.ankushsachdeva.emojicon.EmojiconTextView @@ -45,7 +53,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:autoLink="web" - android:textColor="@color/black87" + android:textColorLink="@color/white" + android:textColor="@color/white" + android:textColorHighlight="@color/grey800" android:textSize="?attr/TextSizeBody" emojicon:emojiconSize="28sp" /> @@ -59,8 +69,9 @@ <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="left" android:orientation="horizontal" - android:paddingTop="1dp" > + android:paddingBottom="2dp"> <ImageView android:id="@+id/security_indicator" @@ -68,9 +79,9 @@ android:layout_height="?attr/TextSizeInfo" android:layout_gravity="center_vertical" android:layout_marginRight="4sp" - android:alpha="0.54" + android:alpha="0.70" android:gravity="center_vertical" - android:src="@drawable/ic_secure_indicator" /> + android:src="@drawable/ic_secure_indicator_white" /> <TextView android:id="@+id/message_time" @@ -79,21 +90,10 @@ android:layout_gravity="center_vertical" android:gravity="center_vertical" android:text="@string/sending" - android:textColor="@color/black54" + android:textColor="@color/white70" android:textSize="?attr/TextSizeInfo" /> </LinearLayout> </LinearLayout> </LinearLayout> - <ImageView - android:id="@+id/message_photo" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_alignParentLeft="true" - android:layout_alignParentTop="true" - android:layout_marginRight="-1.5dp" - android:padding="0dp" - android:scaleType="fitXY" - android:src="@drawable/ic_profile" /> - </RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index 563e2e29..cf7322ee 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -1,14 +1,25 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:emojicon="http://schemas.android.com/apk/res-auto" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingBottom="4dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="4dp" > +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:emojicon="http://schemas.android.com/apk/res-auto" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="3dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="3dp"> + + <ImageView + android:id="@+id/message_photo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="fitXY" + android:paddingBottom="3dp" + android:src="@drawable/ic_profile" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true"" /> <LinearLayout android:id="@+id/message_box" @@ -16,28 +27,26 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_toLeftOf="@+id/message_photo" - android:background="@drawable/message_border" - android:minHeight="48dp" + android:background="@drawable/message_bubble_sent" + android:minHeight="53dp" + android:layout_marginLeft="-4dp" android:longClickable="true"> <LinearLayout android:layout_width="wrap_content" android:layout_height="fill_parent" - android:background="@color/grey50" android:gravity="center_vertical" android:orientation="vertical" - android:paddingBottom="4dp" - android:paddingLeft="5dp" - android:paddingRight="5dp" - android:paddingTop="4dp" > + android:padding="2dp"> <ImageView android:id="@+id/message_image" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="4dp" android:adjustViewBounds="true" android:background="@color/black87" - android:paddingBottom="2dp" android:scaleType="centerCrop" /> <github.ankushsachdeva.emojicon.EmojiconTextView @@ -45,11 +54,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:autoLink="web" + android:textColorLink="@color/black87" android:textColor="@color/black87" + android:textColorHighlight="@color/grey500" android:textSize="?attr/TextSizeBody" emojicon:emojiconSize="28sp" /> - - <Button + + <Button android:id="@+id/download_button" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" @@ -61,7 +72,7 @@ android:layout_height="wrap_content" android:layout_gravity="right" android:orientation="horizontal" - android:paddingTop="1dp" > + android:paddingBottom="2dp"> <TextView android:id="@+id/message_time" @@ -96,15 +107,4 @@ </LinearLayout> </LinearLayout> - <ImageView - android:id="@+id/message_photo" - android:layout_width="48dp" - android:layout_height="48dp" - android:layout_alignParentBottom="true" - android:layout_alignParentRight="true" - android:layout_marginLeft="-1.5dp" - android:padding="0dp" - android:scaleType="fitXY" - android:src="@drawable/ic_profile" /> - </RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/message_status.xml b/src/main/res/layout/message_status.xml index 2816f475..ad2579fa 100644 --- a/src/main/res/layout/message_status.xml +++ b/src/main/res/layout/message_status.xml @@ -1,14 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingBottom="6dp" - android:paddingLeft="8dp" - android:paddingRight="6dp" - android:paddingTop="6dp" > + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="5dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="5dp"> - <ImageView + <com.makeramen.roundedimageview.RoundedImageView android:id="@+id/message_photo" android:layout_width="32dp" android:layout_height="32dp" @@ -17,7 +18,8 @@ android:layout_marginRight="-1.5dp" android:padding="0dp" android:scaleType="fitXY" - android:src="@drawable/ic_profile" /> + android:src="@drawable/ic_profile" + app:riv_corner_radius="1dp"/> <TextView android:id="@+id/status_message" diff --git a/src/main/res/menu/editaccount.xml b/src/main/res/menu/editaccount.xml index 4ce9e1f3..2076805e 100644 --- a/src/main/res/menu/editaccount.xml +++ b/src/main/res/menu/editaccount.xml @@ -20,4 +20,8 @@ <item android:id="@+id/action_change_password_on_server" android:title="@string/change_password" android:showAsAction="never" /> + + <item android:id="@+id/action_clear_devices" + android:title="@string/clear_other_devices" + android:showAsAction="never"/> </menu>
\ No newline at end of file diff --git a/src/main/res/menu/encryption_choices.xml b/src/main/res/menu/encryption_choices.xml index adf0ad8d..9af2cd34 100644 --- a/src/main/res/menu/encryption_choices.xml +++ b/src/main/res/menu/encryption_choices.xml @@ -6,6 +6,9 @@ android:id="@+id/encryption_choice_none" android:title="@string/encryption_choice_none"/> <item + android:id="@+id/encryption_choice_axolotl" + android:title="@string/encryption_choice_omemo"/> + <item android:id="@+id/encryption_choice_otr" android:title="@string/encryption_choice_otr"/> <item diff --git a/src/main/res/menu/muc_details_context.xml b/src/main/res/menu/muc_details_context.xml index dc0f5d3e..af5e691b 100644 --- a/src/main/res/menu/muc_details_context.xml +++ b/src/main/res/menu/muc_details_context.xml @@ -3,30 +3,38 @@ <item android:id="@+id/start_conversation" android:title="@string/start_conversation" - android:visible="false" /> - <item - android:id="@+id/give_membership" + android:visible="false"/> + <item + android:id="@+id/action_contact_details" + android:title="@string/action_contact_details" + android:visible="false"/> + <item + android:id="@+id/send_private_message" + android:title="@string/send_private_message" + android:visible="false"/> + <item + android:id="@+id/give_membership" android:title="@string/grant_membership" - android:visible="false" /> - <item - android:id="@+id/give_admin_privileges" + android:visible="false"/> + <item + android:id="@+id/give_admin_privileges" android:title="@string/grant_admin_privileges" - android:visible="false"/> - <item - android:id="@+id/remove_admin_privileges" + android:visible="false"/> + <item + android:id="@+id/remove_admin_privileges" android:title="@string/remove_admin_privileges" - android:visible="false"/> + android:visible="false"/> - <item - android:id="@+id/remove_membership" - android:title="@string/remove_membership" - android:visible="false"/> - <item - android:id="@+id/ban_from_conference" - android:title="@string/ban_from_conference" - android:visible="false" /> - <item - android:id="@+id/remove_from_room" + <item + android:id="@+id/remove_membership" + android:title="@string/remove_membership" + android:visible="false"/> + <item + android:id="@+id/ban_from_conference" + android:title="@string/ban_from_conference" + android:visible="false"/> + <item + android:id="@+id/remove_from_room" android:title="@string/remove_from_room" - android:visible="false"/> -</menu>
\ No newline at end of file + android:visible="false"/> +</menu> diff --git a/src/main/res/values-ar-rEG/strings.xml b/src/main/res/values-ar-rEG/strings.xml index 79590fd6..5c94b966 100644 --- a/src/main/res/values-ar-rEG/strings.xml +++ b/src/main/res/values-ar-rEG/strings.xml @@ -104,7 +104,6 @@ <string name="pref_vibrate_summary">تفعيل الاهتزاز عندما تصل رساله جديده</string> <string name="pref_sound">التبيه الصوتي</string> <string name="pref_sound_summary">سماع صوت عندما تصل رساله</string> - <string name="pref_conference_notifications">الاشعارات من الغرف</string> <string name="pref_advanced_options">اعدادات متقدمّة</string> <string name="pref_never_send_crash">لا ترسل تقارير أخطاء</string> <string name="pref_never_send_crash_summary">الغاء ارسال تقارير الأخطاء يقلل من فرص حل المشكلة سريعا فكن متعاون</string> diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 73e64331..1ee8a7d6 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -76,9 +76,10 @@ <string name="delete_messages">Изтриване на съобщенията</string> <string name="also_end_conversation">Този разговор да приключи след това</string> <string name="choose_presence">Изберете присъствие за контакта</string> - <string name="send_plain_text_message">Изпращане на обикновено текстово съобщение</string> - <string name="send_otr_message">Изпращане на съобщение, шифровано чрез OTP</string> - <string name="send_pgp_message">Изпращане на съобщение, шифровано чрез OpenPGP</string> + <string name="send_plain_text_message">Изпр. на обикновено текстово съобщение</string> + <string name="send_otr_message">Изпр. на съобщение, шифр. чрез OTP</string> + <string name="send_omemo_message">Изпр. на съобщение, шифр. чрез OMEMO</string> + <string name="send_pgp_message">Изпр. на съобщение, шифр. чрез OpenPGP</string> <string name="your_nick_has_been_changed">Псевдонимът Ви беше променен</string> <string name="send_unencrypted">Изпращане нешифровано</string> <string name="decryption_failed">Неуспешно дешифроване. Възможно е да нямате правилния частен ключ.</string> @@ -105,8 +106,8 @@ <string name="pref_vibrate_summary">Също така да има и вибрация при получаване на ново съобщение</string> <string name="pref_sound">Звук</string> <string name="pref_sound_summary">Изпълнение на звук с известието</string> - <string name="pref_conference_notifications">Известия за беседите</string> - <string name="pref_conference_notifications_summary">Известяване винаги, когато пристигне ново съобщение в беседа, а не само когато тя е отбелязана</string> + <string name="pref_conference_notifications">Известия в публичните беседи</string> + <string name="pref_conference_notifications_summary">Известяване винаги, когато пристигне ново съобщение в публична беседа, а не само когато тя е отбелязана</string> <string name="pref_notification_grace_period">Продължителност на отсрочване на известията</string> <string name="pref_notification_grace_period_summary">Изключва известията за кратко, след като бъде получено копие на съобщение</string> <string name="pref_advanced_options">Разширени настройки</string> @@ -152,6 +153,7 @@ <string name="encryption_choice_none">Обикновен текст</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> <string name="mgmt_account_edit">Редактиране на профила</string> <string name="mgmt_account_delete">Изтриване на профила</string> <string name="mgmt_account_disable">Временно деактивиране</string> @@ -199,11 +201,18 @@ <string name="last_seen_days">последно видян преди %d дни</string> <string name="never_seen">не е виждан никога</string> <string name="install_openkeychain">Шифровано съобщение. Моля, инсталирайте OpenKeychain, за да го дешифровате.</string> - <string name="unknown_otr_fingerprint">Непознат OTR отпечатък</string> + <string name="unknown_otr_fingerprint">Непознат отпечатък OTR</string> <string name="openpgp_messages_found">Открити са съобщения, шифровани чрез OpenPGP</string> <string name="reception_failed">Неуспешно получаване</string> <string name="your_fingerprint">Вашият отпечатък</string> - <string name="otr_fingerprint">OTR отпечатък</string> + <string name="otr_fingerprint">Отпечатък OTR</string> + <string name="omemo_fingerprint">Отпечатък OMEMO</string> + <string name="omemo_fingerprint_selected_message">Отпечатък OMEMO на съобщението</string> + <string name="this_device_omemo_fingerprint">Собствен отпечатък OMEMO</string> + <string name="other_devices">Други устройства</string> + <string name="trust_omemo_fingerprints">Доверяване на отпечатъци OMEMO</string> + <string name="fetching_keys">Изтегляне на ключовете...</string> + <string name="done">Готово</string> <string name="verify">Потвърждаване</string> <string name="decrypt">Дешифроване</string> <string name="conferences">Беседи</string> @@ -283,7 +292,8 @@ <string name="pref_expert_options_other">Други</string> <string name="pref_conference_name">Име на беседата</string> <string name="pref_conference_name_summary">Използване на темата на стаята вместо JID идентификатора за беседите</string> - <string name="toast_message_otr_fingerprint">OTR отпечатъкът е копиран!</string> + <string name="toast_message_otr_fingerprint">Отпечатъкът OTR е копиран!</string> + <string name="toast_message_omemo_fingerprint">Отпечатъкът OMEMO е копиран!</string> <string name="conference_banned">Достъпът Ви до тази беседа беше забранен</string> <string name="conference_members_only">Тази беседа е само за членове</string> <string name="conference_kicked">Бяхте изритан от тази конференция</string> @@ -326,7 +336,7 @@ <string name="pref_keep_foreground_service_summary">Предотвратява прекъсването на връзката Ви от операционната система</string> <string name="choose_file">Изберете файл</string> <string name="receiving_x_file">Получаване на %1$s (%2$d%% завършено)</string> - <string name="download_x_file">Изтегляне на %s</string> + <string name="download_x_file">Сваляне на %s</string> <string name="file">файл</string> <string name="open_x_file">Отваряне на %s</string> <string name="sending_file">изпращане (%1$d%% завършено)</string> @@ -338,7 +348,7 @@ <string name="no_application_found_to_open_file">Няма намерено приложение за отваряне на файла</string> <string name="could_not_verify_fingerprint">Неуспешна проверка на отпечатъка</string> <string name="manually_verify">Ръчна проверка</string> - <string name="are_you_sure_verify_fingerprint">Сигурни ли сте, че искате да проверите OTR отпечатъка на контактите си?</string> + <string name="are_you_sure_verify_fingerprint">Сигурни ли сте, че искате да проверите отпечатъка OTR на контактите си?</string> <string name="pref_show_dynamic_tags">Динамични етикети</string> <string name="pref_show_dynamic_tags_summary">Показване на етикети, предназначени само за четене под контактите</string> <string name="enable_notifications">Включване на известията</string> @@ -349,7 +359,15 @@ <string name="secret_accepted">Тайната е приета!</string> <string name="reset">Възстановяване</string> <string name="account_image_description">Аватар на профила</string> - <string name="copy_otr_clipboard_description">Копиране на OTR отпечатъка</string> + <string name="copy_otr_clipboard_description">Копиране на отпечатъка OTR</string> + <string name="copy_omemo_clipboard_description">Копиране на отпечатъка OMEMO</string> + <string name="regenerate_omemo_key">Повторно създаване на ключа OMEMO</string> + <string name="wipe_omemo_pep">Изчистване на другите устройства от PEP</string> + <string name="clear_other_devices">Премахване на устройствата</string> + <string name="clear_other_devices_desc">Сигурни ли сте, че искате да премахнете всички останали устройства от обявлението OMEMO? Следващия път, когато устройствата Ви се свържат, те ще обявят себе си отново, но може да не получат съобщенията, изпратени междувременно.</string> + <string name="purge_key">Изтриване на ключа</string> + <string name="purge_key_desc_part1">Сигурни ли сте, че искате да изтриете този ключ?</string> + <string name="purge_key_desc_part2">След това той ще бъде необратимо приет за грешен и повече няма да можете да създавате сесии с него.</string> <string name="fetching_history_from_server">Получаване на историята от сървъра</string> <string name="no_more_history_on_server">Няма повече история на сървъра</string> <string name="updating">Актуализиране...</string> @@ -427,7 +445,6 @@ <string name="received_location">Получено местоположение</string> <string name="title_undo_swipe_out_conversation">Conversation се затвори</string> <string name="title_undo_swipe_out_muc">Напуснахте беседата</string> - <string name="pref_certificate_options">Настройки на сертификата</string> <string name="pref_dont_trust_system_cas_title">Да не се вярва на системните сертификати</string> <string name="pref_dont_trust_system_cas_summary">Всички сертификати трябва да бъдат одобрени на ръка</string> <string name="pref_remove_trusted_certificates_title">Премахване на сертификатите</string> @@ -449,6 +466,13 @@ <string name="none">Нищо</string> <string name="recently_used">Използвани наскоро</string> <string name="choose_quick_action">Изберете бързо действие</string> - <string name="file_not_found_on_remote_host">Файлът не е открит на отдалечения сървър</string> <string name="search_for_contacts_or_groups">Търсене на контакти или групи</string> + <string name="send_private_message">Изпращане на лично съобщение</string> + <string name="user_has_left_conference">%s напусна беседата!</string> + <string name="username">Потребителско име</string> + <string name="username_hint">Потребителско име</string> + <string name="invalid_username">Това не е правилно потребителско име</string> + <string name="download_failed_server_not_found">Неуспешно сваляне: Сървърът не е открит</string> + <string name="download_failed_file_not_found">Неуспешно сваляне: Файлът не е открит</string> + <string name="download_failed_could_not_connect">Неуспешно сваляне: Неуспешна връзка със сървъра</string> </resources> diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 6b673071..74d87736 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -97,7 +97,7 @@ <string name="pref_xmpp_resource">Recursos XMPP</string> <string name="pref_xmpp_resource_summary">El nom que identifica aquest client amb</string> <string name="pref_accept_files">Acceptar fitxers</string> - <string name="pref_accept_files_size_summary">Accepta fitxers automàticament amb una mida menor a…</string> + <string name="pref_accept_files_summary">Accepta fitxers automàticament amb una mida menor a…</string> <string name="pref_notification_settings">Ajustos de notificacions</string> <string name="pref_notifications">Notificacions</string> <string name="pref_notifications_summary">Notifica quan arriba un nou missatge</string> @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Vibra quan arriba un nou missatge</string> <string name="pref_sound">So</string> <string name="pref_sound_summary">Reprodueix el to de trucada amb la notificació</string> - <string name="pref_conference_notifications">Notificacions de conferència</string> - <string name="pref_conference_notifications_summary">Sempre notifica quan arriba un nou missatge de conferència en comptes de només quan està destacat</string> <string name="pref_notification_grace_period">Notificació del període d\'espera</string> <string name="pref_notification_grace_period_summary">Desactiva les notificacions durant un breu termini després de rebre una còpia de missatges carbon</string> <string name="pref_advanced_options">Opcions avançades</string> @@ -424,7 +422,6 @@ <string name="received_location">Localització rebuda</string> <string name="title_undo_swipe_out_conversation">Conversa tancada</string> <string name="title_undo_swipe_out_muc">S\'ha sortit de la conferència</string> - <string name="pref_certificate_options">Opcions de certificats</string> <string name="pref_dont_trust_system_cas_title">No confiar en les CAs del sistema</string> <string name="pref_dont_trust_system_cas_summary">Tots els certificats han de ser aprovats manualment</string> <string name="pref_remove_trusted_certificates_title">Eliminar certificats</string> diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 816abba8..2cdf06a9 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -78,6 +78,7 @@ <string name="choose_presence">Vybrat aktualizaci stavu pro kontakt</string> <string name="send_plain_text_message">Poslat textovou zprávu</string> <string name="send_otr_message">Poslat OTR šifrovanou zprávu</string> + <string name="send_omemo_message">Poslat OMEMO šifrovanou zprávu</string> <string name="send_pgp_message">Poslat OpenPGP šifrovanou zprávu</string> <string name="your_nick_has_been_changed">Přezdívka byla změněna</string> <string name="send_unencrypted">Poslat nešifrované</string> @@ -97,7 +98,7 @@ <string name="pref_xmpp_resource">XMPP zdroj</string> <string name="pref_xmpp_resource_summary">Jméno se kterým se tento klient identifikuje</string> <string name="pref_accept_files">Přijímat soubory</string> - <string name="pref_accept_files_size_summary">Automaticky přijímat soubory menší než…</string> + <string name="pref_accept_files_summary">Automaticky přijímat soubory menší než…</string> <string name="pref_notification_settings">Nastavení upozornění</string> <string name="pref_notifications">Upozornění</string> <string name="pref_notifications_summary">Upozornit při přijetí nové zprávy</string> @@ -105,8 +106,8 @@ <string name="pref_vibrate_summary">Vibrovat při přijetí nové zprávy</string> <string name="pref_sound">Zvuk</string> <string name="pref_sound_summary">Přehrát zvuk společně s upozorněním</string> - <string name="pref_conference_notifications">Upozornění při konferencích</string> - <string name="pref_conference_notifications_summary">Vždy upozorňovat při nové konferenční zprávě, nejen pokud je vybrána</string> + <string name="pref_conference_notifications">Upozornění ve veřejných konferencích</string> + <string name="pref_conference_notifications_summary">Vždy upozornit při příchodu zprávy ve veřejných konferencích místo jen pouze pokud je zvýrazněno</string> <string name="pref_notification_grace_period">Četnost upozornění</string> <string name="pref_notification_grace_period_summary">Neupozorňovat krátce poté co byla obdržena kopie zprávy</string> <string name="pref_advanced_options">Pokročilé nastavení</string> @@ -152,6 +153,7 @@ <string name="encryption_choice_none">Čistý text</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> <string name="mgmt_account_edit">Upravit účet</string> <string name="mgmt_account_delete">Smazat účet</string> <string name="mgmt_account_disable">Dočasně vypnout</string> @@ -204,6 +206,13 @@ <string name="reception_failed">Příjem selhal</string> <string name="your_fingerprint">Váš identifikátor</string> <string name="otr_fingerprint">OTR identifikátor</string> + <string name="omemo_fingerprint">OMEMO otisk</string> + <string name="omemo_fingerprint_selected_message">OMEMO otisk zprávy</string> + <string name="this_device_omemo_fingerprint">Můj OMEMO otisk</string> + <string name="other_devices">Ostatní přístroje</string> + <string name="trust_omemo_fingerprints">Věřit OMEMO otiskům</string> + <string name="fetching_keys">Získávám klíče...</string> + <string name="done">Hotovo</string> <string name="verify">Ověřit</string> <string name="decrypt">Dešifrovat</string> <string name="conferences">Konference</string> @@ -284,6 +293,7 @@ <string name="pref_conference_name">Jméno konference</string> <string name="pref_conference_name_summary">Pro identifikaci konferencí použít téma místnosti místo jejího JID</string> <string name="toast_message_otr_fingerprint">OTR otisk zkopírován do schránky!</string> + <string name="toast_message_omemo_fingerprint">OMEMO otisk zkopírován do schránky!</string> <string name="conference_banned">Vstup do konference byl zakázán</string> <string name="conference_members_only">Tato konference je pouze pro členy</string> <string name="conference_kicked">Vykopli tě z této konference</string> @@ -350,6 +360,14 @@ <string name="reset">Reset</string> <string name="account_image_description">Avatar účtu</string> <string name="copy_otr_clipboard_description">Zkopírovat otisk OTR do schránky</string> + <string name="copy_omemo_clipboard_description">Zkopírovat OMEMO otisk do schránky</string> + <string name="regenerate_omemo_key">Znovu vytvořit OMEMO klíč</string> + <string name="wipe_omemo_pep">Smazat ostatní přístroje z PEP</string> + <string name="clear_other_devices">Smazat přístroje</string> + <string name="clear_other_devices_desc">Opravdu chcete vymazat ostatní přístroje z OMEMO upozornění? Až se příště vaše přístroje připojí, znovu se ohlásí, ale pravděpodobně neobdrží zprávy odeslané v mezičase mezi přihlášeními.</string> + <string name="purge_key">Vymazat klíč</string> + <string name="purge_key_desc_part1">Opravdu si přejete vymazat tento klíč?</string> + <string name="purge_key_desc_part2">Bude neodvolatelně pokládán jako kompromitovaný a již s jeho pomocí nebudete moci nikdy spustit jiné sezení.</string> <string name="fetching_history_from_server">Načíst historii ze serveru</string> <string name="no_more_history_on_server">Na serveru není žádná další historie</string> <string name="updating">Aktualizuji...</string> @@ -427,7 +445,6 @@ <string name="received_location">Přijmout pozici</string> <string name="title_undo_swipe_out_conversation">Conversation zavřena</string> <string name="title_undo_swipe_out_muc">Opustil(a) konferenci</string> - <string name="pref_certificate_options">Nastavení certifikátu</string> <string name="pref_dont_trust_system_cas_title">Nedůvěřovat systémovým CA</string> <string name="pref_dont_trust_system_cas_summary">Všechny certifikáty musí být schváleny ručně</string> <string name="pref_remove_trusted_certificates_title">Odstranit certifikáty</string> @@ -451,6 +468,13 @@ <string name="none">Žádná</string> <string name="recently_used">Naposledy použitá</string> <string name="choose_quick_action">Vybrat rychlou akci</string> - <string name="file_not_found_on_remote_host">Soubor nenalezen na vzdáleném serveru</string> <string name="search_for_contacts_or_groups">Hledat kontakty či skupiny</string> + <string name="send_private_message">Poslat soukromou zprávu</string> + <string name="user_has_left_conference">%s opustil(a) konferenci!</string> + <string name="username">Uživatelské jméno</string> + <string name="username_hint">Uživatelské jméno</string> + <string name="invalid_username">Toto není platné uživatelské jméno</string> + <string name="download_failed_server_not_found">Stahování selhalo: Server nenalezen</string> + <string name="download_failed_file_not_found">Stahování selhalo: Soubor nenalezen</string> + <string name="download_failed_could_not_connect">Stahování selhalo: Nelze se připojit k hostu</string> </resources> diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 2d5337be..f1cf580d 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -78,6 +78,7 @@ <string name="choose_presence">Ressource des Kontakts auswählen</string> <string name="send_plain_text_message">Normal schreiben…</string> <string name="send_otr_message">OTR-verschlüsselt schreiben…</string> + <string name="send_omemo_message">OMEMO-verschlüsselt schreiben…</string> <string name="send_pgp_message">OpenPGP-verschlüsselt schreiben…</string> <string name="your_nick_has_been_changed">Dein Nickname wurde geändert</string> <string name="download_image">Bild herunterladen</string> @@ -94,10 +95,10 @@ <string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string> <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sage deinen Kontakten, sie mögen OpenPGP einrichten.</small></string> <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie zu entschlüsseln und anzuzeigen.</i></string> - <string name="pref_general">Allgemeines</string> - <string name="pref_xmpp_resource">XMPP-Ressource</string> - <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string> - <string name="pref_accept_files">Dateiannahme</string> + <string name="pref_general">Allgemeines</string> + <string name="pref_xmpp_resource">XMPP-Ressource</string> + <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string> + <string name="pref_accept_files">Dateiannahme</string> <string name="pref_accept_files_summary">Einstellungen für Dateiannahme und automatischen Download</string> <string name="pref_accept_files_size">Größe</string> <string name="pref_accept_files_size_summary">Dateien, die kleiner sind als …, automatisch annehmen</string> @@ -105,40 +106,40 @@ <string name="pref_accept_files_download_summary">Automatisches Herunterladen und Akzpetieren von Dateien nur im WLAN</string> <string name="pref_accept_files_download_link">Bilder-Links</string> <string name="pref_accept_files_download_link_summary">Bilder-Links automatisch herunterladen</string> - <string name="pref_notification_settings">Benachrichtigungen</string> - <string name="pref_notifications">Benachrichtigungen</string> - <string name="pref_notifications_summary">Benachrichtige mich, wenn eine neue Nachricht ankommt</string> - <string name="pref_vibrate">Vibrieren</string> - <string name="pref_vibrate_summary">Vibriere, wenn eine neue Nachricht ankommt</string> - <string name="pref_sound">Klingelton</string> - <string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string> - <string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string> - <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenz-Nachricht und nicht nur, wenn ich angesprochen werde</string> - <string name="pref_notification_grace_period">Gnadenfrist</string> - <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string> - <string name="pref_advanced_options">Erweiterte Optionen</string> - <string name="pref_never_send_crash">Sende niemals Absturzberichte</string> - <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string> + <string name="pref_notification_settings">Benachrichtigungen</string> + <string name="pref_notifications">Benachrichtigungen</string> + <string name="pref_notifications_summary">Benachrichtige mich, wenn eine neue Nachricht ankommt</string> + <string name="pref_vibrate">Vibrieren</string> + <string name="pref_vibrate_summary">Vibriere, wenn eine neue Nachricht ankommt</string> + <string name="pref_sound">Klingelton</string> + <string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string> + <string name="pref_conference_notifications">öffentliche Konferenz-Benachrichtigungen</string> + <string name="pref_conference_notifications_summary">Bei jeder Nachricht in öffentlichen Konferenzen benachrichtigen und nicht nur, wenn ich angesprochen werde</string> + <string name="pref_notification_grace_period">Gnadenfrist</string> + <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string> + <string name="pref_advanced_options">Erweiterte Optionen</string> + <string name="pref_never_send_crash">Sende niemals Absturzberichte</string> + <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string> <string name="pref_confirm_messages">Lese- und Empfangsbestätigung senden</string> <string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfangen oder gelesen hast</string> <string name="pref_confirm_messages_none">Keine Bestätigungen</string> <string name="pref_confirm_messages_received">Nur Empfangsbestätigung</string> <string name="pref_confirm_messages_read_and_received">Lese- und Empfangsbestätigung</string> - <string name="pref_ui_options">Benutzeroberfläche</string> + <string name="pref_ui_options">Benutzeroberfläche</string> <string name="pref_parse_emoticons">Smilies ersetzen</string> <string name="pref_parse_emoticons_summary">Zeige Smilie-Bilder anstelle von Emoticons.</string> - <string name="openpgp_error">Fehler mit OpenKeychain</string> - <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string> - <string name="accept">Annehmen</string> - <string name="error">Ein unbekannter Fehler ist aufgetreten</string> - <string name="pref_grant_presence_updates">Online-Status</string> - <string name="pref_grant_presence_updates_summary">Erlaube neu hinzugefügten Kontakten deinen online-Status zu sehen und frage um Erlaubnis, ihren sehen zu dürfen</string> - <string name="subscriptions">Abonnements</string> - <string name="your_account">Dein Konto</string> - <string name="keys">Schlüssel</string> - <string name="send_presence_updates">Anwesenheitsbenachrichtigungen senden</string> - <string name="receive_presence_updates">Empfange Anwesenheitsbenachrichtigungen</string> - <string name="ask_for_presence_updates">Frage um Erlaubnis, Anwesenheitsbenachrichtigungen sehen zu dürfen</string> + <string name="openpgp_error">Fehler mit OpenKeychain</string> + <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string> + <string name="accept">Annehmen</string> + <string name="error">Ein unbekannter Fehler ist aufgetreten</string> + <string name="pref_grant_presence_updates">Online-Status</string> + <string name="pref_grant_presence_updates_summary">Erlaube neu hinzugefügten Kontakten meinen Online-Status zu sehen und frage um Erlaubnis, ihren sehen zu dürfen</string> + <string name="subscriptions">Abonnements</string> + <string name="your_account">Dein Konto</string> + <string name="keys">Schlüssel</string> + <string name="send_presence_updates">Online-Status senden</string> + <string name="receive_presence_updates">Online-Status empfangen</string> + <string name="ask_for_presence_updates">Online-Status anfragen</string> <string name="attach_choose_picture">Bild auswählen</string> <string name="attach_take_picture">Bild aufnehmen</string> <string name="preemptively_grant">Erlaube Statusanfrage vorab</string> @@ -151,26 +152,27 @@ <string name="account_status_disabled">Vorübergehend abgeschaltet</string> <string name="account_status_online">Online</string> <string name="account_status_connecting">Verbinde…</string> - <string name="account_status_offline">Offline</string> - <string name="account_status_unauthorized">Ungültige Zugangsdaten</string> - <string name="account_status_not_found">Server nicht gefunden</string> - <string name="account_status_no_internet">Keine Internetverbindung</string> - <string name="account_status_regis_fail">Registrierung fehlgeschlagen</string> - <string name="account_status_regis_conflict">Benutzername wird bereits verwendet</string> - <string name="account_status_regis_success">Registrierung abgeschlossen</string> - <string name="account_status_regis_not_sup">Der Server unterstützt keine Registrierung</string> - <string name="account_status_security_error">Sicherheitsfehler</string> - <string name="account_status_incompatible_server">Inkompatibler Server</string> - <string name="encryption_choice_none">Klartext</string> - <string name="encryption_choice_otr">OTR</string> - <string name="encryption_choice_pgp">OpenPGP</string> - <string name="mgmt_account_edit">Konto bearbeiten</string> - <string name="mgmt_account_delete">Löschen</string> - <string name="mgmt_account_disable">Vorübergehend abschalten</string> - <string name="mgmt_account_publish_avatar">Avatar veröffentlichen</string> - <string name="mgmt_account_publish_pgp">Öffentlichen OpenPGP-Schlüssel veröffentlichen</string> - <string name="mgmt_account_enable">Anschalten</string> - <string name="mgmt_account_are_you_sure">Bist du dir sicher?</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Ungültige Zugangsdaten</string> + <string name="account_status_not_found">Server nicht gefunden</string> + <string name="account_status_no_internet">Keine Internetverbindung</string> + <string name="account_status_regis_fail">Registrierung fehlgeschlagen</string> + <string name="account_status_regis_conflict">Benutzername wird bereits verwendet</string> + <string name="account_status_regis_success">Registrierung abgeschlossen</string> + <string name="account_status_regis_not_sup">Der Server unterstützt keine Registrierung</string> + <string name="account_status_security_error">Sicherheitsfehler</string> + <string name="account_status_incompatible_server">Inkompatibler Server</string> + <string name="encryption_choice_none">Klartext</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> + <string name="mgmt_account_edit">Konto bearbeiten</string> + <string name="mgmt_account_delete">Löschen</string> + <string name="mgmt_account_disable">Vorübergehend abschalten</string> + <string name="mgmt_account_publish_avatar">Avatar veröffentlichen</string> + <string name="mgmt_account_publish_pgp">Öffentlichen OpenPGP-Schlüssel veröffentlichen</string> + <string name="mgmt_account_enable">Anschalten</string> + <string name="mgmt_account_are_you_sure">Bist du dir sicher?</string> <string name="mgmt_account_delete_confirm_text">Wenn du dein Konto löschst, gehen alle Gesprächsverläufe verloren</string> <string name="attach_record_voice">Sprache aufzeichnen</string> <string name="account_settings_jabber_id">Jabber-ID</string> @@ -201,26 +203,33 @@ <string name="server_info_pep">XEP-0163: PEP (Avatare)</string> <string name="server_info_available">ja</string> <string name="server_info_unavailable">nein</string> - <string name="missing_public_keys">Öffentlicher Schlüssel fehlt</string> - <string name="last_seen_now">online</string> - <string name="last_seen_min">vor einer Minute gesehen</string> - <string name="last_seen_mins">vor %d Minuten gesehen</string> - <string name="last_seen_hour">vor einer Stunde gesehen</string> - <string name="last_seen_hours">vor %d Stunden gesehen</string> - <string name="last_seen_day">vor einem Tag gesehen</string> - <string name="last_seen_days">vor %d Tagen gesehen</string> - <string name="never_seen">noch nie gesehen</string> - <string name="install_openkeychain">Verschlüsselte Nachricht. Bitte installiere OpenKeychain zur Entschlüsselung.</string> - <string name="unknown_otr_fingerprint">Unbekannter OTR-Fingerabdruck</string> - <string name="openpgp_messages_found">Verschlüsselte OpenPGP-Nachricht gefunden</string> - <string name="reception_failed">Empfang ist fehlgeschlagen</string> - <string name="your_fingerprint">Dein Fingerabdruck</string> - <string name="otr_fingerprint">OTR-Fingerabdruck</string> - <string name="verify">Verifizieren</string> - <string name="decrypt">Entschlüsseln</string> - <string name="conferences">Konferenzen</string> - <string name="search">Suche</string> - <string name="create_contact">Kontakt erstellen</string> + <string name="missing_public_keys">Öffentlicher Schlüssel fehlt</string> + <string name="last_seen_now">online</string> + <string name="last_seen_min">vor einer Minute gesehen</string> + <string name="last_seen_mins">vor %d Minuten gesehen</string> + <string name="last_seen_hour">vor einer Stunde gesehen</string> + <string name="last_seen_hours">vor %d Stunden gesehen</string> + <string name="last_seen_day">vor einem Tag gesehen</string> + <string name="last_seen_days">vor %d Tagen gesehen</string> + <string name="never_seen">noch nie gesehen</string> + <string name="install_openkeychain">Verschlüsselte Nachricht. Bitte installiere OpenKeychain zur Entschlüsselung.</string> + <string name="unknown_otr_fingerprint">Unbekannter OTR-Fingerabdruck</string> + <string name="openpgp_messages_found">Verschlüsselte OpenPGP-Nachricht gefunden</string> + <string name="reception_failed">Empfang ist fehlgeschlagen</string> + <string name="your_fingerprint">Dein Fingerabdruck</string> + <string name="otr_fingerprint">OTR-Fingerabdruck</string> + <string name="omemo_fingerprint">OMEMO-Fingerabdruck</string> + <string name="omemo_fingerprint_selected_message">OMEMO-Fingerabdruck der Nachricht</string> + <string name="this_device_omemo_fingerprint">Eigener OMEMO-Fingerabdruck</string> + <string name="other_devices">Andere Geräte</string> + <string name="trust_omemo_fingerprints">OMEMO-Fingerabdruck vertrauen</string> + <string name="fetching_keys">Schlüssel abrufen...</string> + <string name="done">Erledigt</string> + <string name="verify">Verifizieren</string> + <string name="decrypt">Entschlüsseln</string> + <string name="conferences">Konferenzen</string> + <string name="search">Suche</string> + <string name="create_contact">Kontakt erstellen</string> <string name="join_conference">Konferenz betreten</string> <string name="delete_contact">Kontakt löschen</string> <string name="view_contact_details">Kontakt-Details anzeigen</string> @@ -292,10 +301,11 @@ <string name="pref_use_indicate_received">Anfrage für Nachrichtenempfang</string> <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häkchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string> <string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string> - <string name="pref_expert_options_other">Sonstiges</string> - <string name="pref_conference_name">Konferenz-Name</string> - <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Namen verwenden</string> - <string name="toast_message_otr_fingerprint">OTR-Fingerabdruck in die Zwischenablage kopiert!</string> + <string name="pref_expert_options_other">Sonstiges</string> + <string name="pref_conference_name">Konferenz-Name</string> + <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Namen verwenden</string> + <string name="toast_message_otr_fingerprint">OTR-Fingerabdruck in die Zwischenablage kopiert!</string> + <string name="toast_message_omemo_fingerprint">OMEMO-Fingerabdruck in die Zwischenablage kopiert!</string> <string name="conference_banned">Du wurdest von der Konferenz ausgeschlossen</string> <string name="conference_members_only">Die Konferenz ist nur für Mitglieder</string> <string name="conference_kicked">Du wurdest aus der Konferenz geworfen</string> @@ -357,11 +367,19 @@ <string name="conference_with">Beginne Konferenz mit…</string> <string name="no_conference_server_found">Konferenz-Server kann nicht gefunden werden</string> <string name="conference_creation_failed">Beginnen der Konferenz fehlgeschlagen!</string> - <string name="conference_created">Konferenz erstellt!</string> - <string name="secret_accepted">Schlüssel akzeptiert!</string> - <string name="reset">Zurücksetzen</string> - <string name="account_image_description">Konto-Avatar</string> - <string name="copy_otr_clipboard_description">OTR-Fingerabdruck in Zwischenablage kopieren</string> + <string name="conference_created">Konferenz erstellt!</string> + <string name="secret_accepted">Schlüssel akzeptiert!</string> + <string name="reset">Zurücksetzen</string> + <string name="account_image_description">Konto-Avatar</string> + <string name="copy_otr_clipboard_description">OTR-Fingerabdruck in Zwischenablage kopieren</string> + <string name="copy_omemo_clipboard_description">OMEMO-Fingerabdruck in Zwischenablage kopieren</string> + <string name="regenerate_omemo_key">OMEMO-Schlüssel erneuern</string> + <string name="wipe_omemo_pep">Andere Geräte von PEP entfernen</string> + <string name="clear_other_devices">Geräte entfernen</string> + <string name="clear_other_devices_desc">Alle anderen Geräte aus der OMEMO-Bekanntmachung entfernen? Die Bekanntmachung wird bei der nächsten Verbindung erneuert, möglicherweise werden aber nicht alle Nachrichten empfangen, die in der Zwischenzeit versendet wurden.</string> + <string name="purge_key">Schlüssel löschen</string> + <string name="purge_key_desc_part1">Soll dieser Schlüssel gelöscht werden?</string> + <string name="purge_key_desc_part2">Dieser Vorgang kann nicht rückgängig gemacht werden und es kann nie wieder eine Verbindung mit diesem Schlüssel hergestellt werden.</string> <string name="fetching_history_from_server">Lade Chatverlauf…</string> <string name="no_more_history_on_server">Keine weiteren Nachrichten vorhanden</string> <string name="updating">Aktualisiere…</string> @@ -440,7 +458,6 @@ <string name="received_location">Standort empfangen</string> <string name="title_undo_swipe_out_conversation">Unterhaltung beendet</string> <string name="title_undo_swipe_out_muc">Konferenz verlassen</string> - <string name="pref_certificate_options">Zertifikats-Optionen</string> <string name="pref_dont_trust_system_cas_title">Misstraue Zertifizierungsstellen</string> <string name="pref_dont_trust_system_cas_summary">Alle Zertifikate müssen manuell bestätigt werden</string> <string name="pref_remove_trusted_certificates_title">Zertifikate löschen</string> @@ -462,8 +479,15 @@ <string name="none">keine</string> <string name="recently_used">zuletzt verwendet</string> <string name="choose_quick_action">wähle Schnell-Taste</string> - <string name="file_not_found_on_remote_host">Datei auf Server nicht gefunden</string> <string name="search_for_contacts_or_groups">Nach Kontakten oder Konferenzen suchen</string> + <string name="send_private_message">Private Nachricht senden</string> + <string name="user_has_left_conference">%s hat die Konferenz verlassen!</string> + <string name="username">Benutzername</string> + <string name="username_hint">Benutzername</string> + <string name="invalid_username">Ungültiger Benutzername</string> + <string name="download_failed_server_not_found">Download fehlgeschlagen: Server nicht gefunden</string> + <string name="download_failed_file_not_found">Download fehlgeschlagen: Datei nicht gefunden</string> + <string name="download_failed_could_not_connect">Download fehlgeschlagen: keine Verbindung zum Host</string> <string name="pref_led_notification_color">LED-Benachrichtigung Farbe</string> <string name="pref_led_notification_color_summary">Setze die Farbe der LED-Benachrichtigung</string> </resources> diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 0e2dc010..b9fc352c 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -97,7 +97,7 @@ <string name="pref_xmpp_resource">πόρος XMPP</string> <string name="pref_xmpp_resource_summary">Το όνομα με το οποίο ταυτοποιείται αυτό το πρόγραμμα-πελάτης</string> <string name="pref_accept_files">Αποδοχή αρχείων</string> - <string name="pref_accept_files_size_summary">Αυτόματη αποδοχή αρχείων μικρότερα από...</string> + <string name="pref_accept_files_summary">Αυτόματη αποδοχή αρχείων μικρότερα από...</string> <string name="pref_notification_settings">Επιλογές ειδοποιήσεων</string> <string name="pref_notifications">Ειδοποιήσεις</string> <string name="pref_notifications_summary">Ειδοποίηση όταν λαμβάνεται ένα νέο μήνυμα</string> @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Επίσης δόνηση όταν έρχεται ένα νέο μήνυμα</string> <string name="pref_sound">Ήχος</string> <string name="pref_sound_summary">Αναπαραγωγή ήχου κλήσης με την ειδοποίηση</string> - <string name="pref_conference_notifications">Ειδοποιήσεις συνδιασκέψεων</string> - <string name="pref_conference_notifications_summary">Ειδοποίηση πάντα όταν ένα νέο μήνυμα συνδιάσκεψης λαμβάνεται αντί για μόνο όταν τονίζεται</string> <string name="pref_notification_grace_period">Περίοδος χάριτος ειδοποιήσεων</string> <string name="pref_notification_grace_period_summary">Απενεργοποίηση ειδοποιήσεων για λίγο χρόνο μετά από τη λήψη ακριβούς αντιγράφου</string> <string name="pref_advanced_options">Προχωρημένες επιλογές</string> @@ -424,7 +422,6 @@ <string name="received_location">Ελήφθη τοποθεσία</string> <string name="title_undo_swipe_out_conversation">Η συζήτηση έκλεισε</string> <string name="title_undo_swipe_out_muc">Έφυγε από την συνδιάσκεψη</string> - <string name="pref_certificate_options">Επιλογές πιστοποιητικών</string> <string name="pref_dont_trust_system_cas_title">Μη έμπιστες αρχές πιστοποίησης συστήματος</string> <string name="pref_dont_trust_system_cas_summary">Όλα τα πιστοποιητικά πρέπει να εγκριθούν χειροκίνητα</string> <string name="pref_remove_trusted_certificates_title">Αφαίρεση πιστοποιητικών</string> diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 4d8bef38..56b7c2e0 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -78,6 +78,7 @@ <string name="choose_presence">Selecciona recurso del contacto</string> <string name="send_plain_text_message">Enviar mensaje de texto</string> <string name="send_otr_message">Enviar mensaje cifrado con OTR</string> + <string name="send_omemo_message">Enviar mensaje cifrado con OMEMO </string> <string name="send_pgp_message">Enviar mensaje cifrado con OpenPGP</string> <string name="your_nick_has_been_changed">Tu apodo se ha modificado</string> <string name="send_unencrypted">Enviar sin cifrar</string> @@ -97,7 +98,7 @@ <string name="pref_xmpp_resource">Recurso</string> <string name="pref_xmpp_resource_summary">El nombre que identifica el cliente que estás utilizando</string> <string name="pref_accept_files">Aceptar archivos</string> - <string name="pref_accept_files_size_summary">De forma automática aceptar archivos menores que…</string> + <string name="pref_accept_files_summary">De forma automática aceptar archivos menores que…</string> <string name="pref_notification_settings">Ajustes de notificación</string> <string name="pref_notifications">Notificaciones</string> <string name="pref_notifications_summary">Notifica cuando llega un nuevo mensaje</string> @@ -105,14 +106,14 @@ <string name="pref_vibrate_summary">Vibra cuando llega un nuevo mensaje</string> <string name="pref_sound">Sonido</string> <string name="pref_sound_summary">Reproduce tono con la notificación</string> - <string name="pref_conference_notifications">Notif. conversación grupo</string> - <string name="pref_conference_notifications_summary">Siempre notifica cuando llega un mensaje a una conversación en grupo y no solo cuando alguien menciona tu nombre en un mensaje</string> + <string name="pref_conference_notifications">Notif. conversación grupo pública</string> + <string name="pref_conference_notifications_summary">Siempre notifica cuando llega un mensaje a una conversación en grupo pública y no solo cuando alguien menciona tu nombre en un mensaje</string> <string name="pref_notification_grace_period">Notificaciones Carbons</string> <string name="pref_notification_grace_period_summary">Deshabilita las notificaciones durante un corto periodo de tiempo después de recibir la copia del mensaje carbon</string> <string name="pref_advanced_options">Opciones avanzadas</string> <string name="pref_never_send_crash">Nunca informar de errores</string> <string name="pref_never_send_crash_summary">Si envías registros de error ayudas al desarrollo de Conversations</string> - <string name="pref_confirm_messages">Confirmar Mensajes</string> + <string name="pref_confirm_messages">Confirmar mensajes</string> <string name="pref_confirm_messages_summary">Permitir a tus contactos saber cuando recibes y lees un mensaje</string> <string name="pref_ui_options">Opciones de interfaz</string> <string name="openpgp_error">OpenKeychain reportó un error</string> @@ -152,6 +153,7 @@ <string name="encryption_choice_none">Texto plano</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> <string name="mgmt_account_edit">Editar cuenta</string> <string name="mgmt_account_delete">Eliminar cuenta</string> <string name="mgmt_account_disable">Deshabilitar temporalmente</string> @@ -204,6 +206,13 @@ <string name="reception_failed">Error al recibir</string> <string name="your_fingerprint">Tu huella digital</string> <string name="otr_fingerprint">Huella digital OTR</string> + <string name="omemo_fingerprint">Huella digital OMEMO</string> + <string name="omemo_fingerprint_selected_message">Huella digital OMEMO del mensaje</string> + <string name="this_device_omemo_fingerprint">Tu huella digital OMEMO</string> + <string name="other_devices">Otros dispositivos</string> + <string name="trust_omemo_fingerprints">Huellas digitales OMEMO de confianza</string> + <string name="fetching_keys">Buscando claves...</string> + <string name="done">Hecho</string> <string name="verify">Verificar</string> <string name="decrypt">Descifrar</string> <string name="conferences">Conversación Grupo</string> @@ -284,6 +293,7 @@ <string name="pref_conference_name">Nombre conversación grupo</string> <string name="pref_conference_name_summary">Usar el asunto de la conversación en lugar del identificador jabber como nombre en las conversaciones en grupo</string> <string name="toast_message_otr_fingerprint">¡Huella digital OTR copiada al portapapeles!</string> + <string name="toast_message_omemo_fingerprint">¡Huella digital OMEMO copiada al portapapeles!</string> <string name="conference_banned">Tu entrada a esta conversación ha sido prohibida</string> <string name="conference_members_only">Esta conversación es solo para miembros</string> <string name="conference_kicked">Has sido expulsado de esta conversación</string> @@ -350,6 +360,14 @@ <string name="reset">Reinicializar</string> <string name="account_image_description">Imagen de perfil</string> <string name="copy_otr_clipboard_description">Copiar huella digital OTR al portapapeles</string> + <string name="copy_omemo_clipboard_description">Copiar huella digital OMEMO al portapapeles</string> + <string name="regenerate_omemo_key">Regenerar clave OMEMO</string> + <string name="wipe_omemo_pep">Limpiar otros dispositivos de PEP</string> + <string name="clear_other_devices">Limpiar dispositivos</string> + <string name="clear_other_devices_desc">¿Estás seguro de que quieres limpiar todos los otros dispositivos del anuncio OMEMO? La próxima vez que tus dispositivos conecten, tendrán que volver a anunciarse, pero estos podrían no recibir los mensajes enviados durante el proceso.</string> + <string name="purge_key">Eliminar clave</string> + <string name="purge_key_desc_part1">¿Estás seguro de que quieres eliminar esta clave?</string> + <string name="purge_key_desc_part2">Esto será irreversible y nunca podrás iniciar sesión con esta clave de nuevo.</string> <string name="fetching_history_from_server">Buscando historial en el servidor</string> <string name="no_more_history_on_server">No hay más historial en el servidor</string> <string name="updating">Actualizando…</string> @@ -427,13 +445,12 @@ <string name="received_location">Ubicación recibida</string> <string name="title_undo_swipe_out_conversation">Conversación cerrada</string> <string name="title_undo_swipe_out_muc">Has salido de la conversación</string> - <string name="pref_certificate_options">Opciones de Certificados</string> <string name="pref_dont_trust_system_cas_title">No confiar en los CAs del sistema</string> <string name="pref_dont_trust_system_cas_summary">Todos los certificados deben ser aprobados manualmente</string> - <string name="pref_remove_trusted_certificates_title">Eliminar Certificados</string> + <string name="pref_remove_trusted_certificates_title">Eliminar certificados</string> <string name="pref_remove_trusted_certificates_summary">Eliminar manualmente certificados aceptados</string> <string name="toast_no_trusted_certs">No aceptar certificados manualmente</string> - <string name="dialog_manage_certs_title">Eliminar Certificados</string> + <string name="dialog_manage_certs_title">Eliminar certificados</string> <string name="dialog_manage_certs_positivebutton">Eliminar seleccionados</string> <string name="dialog_manage_certs_negativebutton">Cancelar</string> <plurals name="toast_delete_certificates"> @@ -445,10 +462,17 @@ <item quantity="other">Seleccionados %d contactos</item> </plurals> <string name="pref_quick_action_summary">Cambiar el botón de enviar por botón de acción rápida</string> - <string name="pref_quick_action">Acción Rápida</string> + <string name="pref_quick_action">Acción rápida</string> <string name="none">Ninguna</string> <string name="recently_used">Usada más recientemente</string> <string name="choose_quick_action">Elegir acción rápida</string> - <string name="file_not_found_on_remote_host">Archivo no encontrado en servidor remoto</string> <string name="search_for_contacts_or_groups">Buscar contactos o grupos</string> + <string name="send_private_message">Enviar mensaje privado</string> + <string name="user_has_left_conference">¡%s ha dejado la conversación!</string> + <string name="username">Usuario</string> + <string name="username_hint">Usuario</string> + <string name="invalid_username">Esto no es un usuario válido</string> + <string name="download_failed_server_not_found">Error al descargar: Servidor no encontrado</string> + <string name="download_failed_file_not_found">Error al descargar: Archivo no encontrado</string> + <string name="download_failed_could_not_connect">Error al descargar: No se ha podido conectar con el servidor</string> </resources> diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index ffc276cc..75b6dc09 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -78,6 +78,7 @@ <string name="choose_presence">Hautatu agerpena kontaktuarentzat</string> <string name="send_plain_text_message">Testu mezua bidali</string> <string name="send_otr_message">OTRz enkriptatutako mezua bidali</string> + <string name="send_omemo_message">OMEMOz enkriptatutako mezua bidali</string> <string name="send_pgp_message">OpenPGPz enkriptatutako mezua bidali</string> <string name="your_nick_has_been_changed">Zure ezizena aldatu da</string> <string name="send_unencrypted">Enkriptatu gabe bidali</string> @@ -97,7 +98,7 @@ <string name="pref_xmpp_resource">XMPP baliabidea</string> <string name="pref_xmpp_resource_summary">Bezero honek bere burua aurkezteko erabiltzen duen izena</string> <string name="pref_accept_files">Fitxategiak onartu</string> - <string name="pref_accept_files_size_summary">Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu…</string> + <string name="pref_accept_files_summary">Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu…</string> <string name="pref_notification_settings">Jakinarazpenen ezarpenak</string> <string name="pref_notifications">Jakinarazpenak</string> <string name="pref_notifications_summary">Mezu berri bat heltzerakoan jakinarazi</string> @@ -105,8 +106,8 @@ <string name="pref_vibrate_summary">Dardaratu ere mezu berri bat heltzerakoan</string> <string name="pref_sound">Soinua</string> <string name="pref_sound_summary">Dei-tonua jo jakinarazpenarekin</string> - <string name="pref_conference_notifications">Konferentzien jakinarazpenak</string> - <string name="pref_conference_notifications_summary">Beti jakinarazi konferentzia mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan</string> + <string name="pref_conference_notifications">Konferentzia publikoen jakinarazpenak</string> + <string name="pref_conference_notifications_summary">Beti jakinarazi konferentzia publiko batean mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan</string> <string name="pref_notification_grace_period">Jakinarazpenen grazia epea</string> <string name="pref_notification_grace_period_summary">Jakinarazpenak denbora labur baterako ezgaitu ikatz-kopia bat jaso ondoren</string> <string name="pref_advanced_options">Aukera aurreratuak</string> @@ -152,6 +153,7 @@ <string name="encryption_choice_none">Testu laua</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> <string name="mgmt_account_edit">Kontua editatu</string> <string name="mgmt_account_delete">Kontua ezabatu</string> <string name="mgmt_account_disable">Aldi baterako ezgaitu</string> @@ -204,6 +206,13 @@ <string name="reception_failed">Jasotzeak huts egin du</string> <string name="your_fingerprint">Zure hatz-marka</string> <string name="otr_fingerprint">OTR hatz-marka</string> + <string name="omemo_fingerprint">OMEMO hatz-marka</string> + <string name="omemo_fingerprint_selected_message">Mezuaren OMEMO hatz-marka</string> + <string name="this_device_omemo_fingerprint">Norberaren OMEMO hatz-marka</string> + <string name="other_devices">Beste gailuak</string> + <string name="trust_omemo_fingerprints">OMEMO hatz-marketaz fidatu</string> + <string name="fetching_keys">Gakoak eskuratzen...</string> + <string name="done">Eginda</string> <string name="verify">Egiaztatu</string> <string name="decrypt">Desenkriptatu</string> <string name="conferences">Konferentziak</string> @@ -284,6 +293,7 @@ <string name="pref_conference_name">Konferentziaren izena</string> <string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko eta ez JIDa</string> <string name="toast_message_otr_fingerprint">OTR hatz-marka arbelara kopiatu da</string> + <string name="toast_message_omemo_fingerprint">OMEMO hatz-marka arbelara kopiatu da</string> <string name="conference_banned">Konferentzia honetara sartzea debekatuta duzu</string> <string name="conference_members_only">Konferentzia hau kideentzat da soilik</string> <string name="conference_kicked">Konferentzia honetatik kanporatua izan zara</string> @@ -350,6 +360,14 @@ <string name="reset">Berrezarri</string> <string name="account_image_description">Kontuaren profileko argazkia</string> <string name="copy_otr_clipboard_description">OTR hatz-marka arbelera kopiatu</string> + <string name="copy_omemo_clipboard_description">OMEMO hatz-marka arbelara kopiatu</string> + <string name="regenerate_omemo_key">OMEMO gakoa birsortu</string> + <string name="wipe_omemo_pep">Beste gailuak PEPetik garbitu</string> + <string name="clear_other_devices">Gailuak garbitu</string> + <string name="clear_other_devices_desc">Ziur al zaude OMEMO iragarpenetik beste gailu guztiak garbitu nahi dituzulaz? Zure gailuak konektatzen diren hurrengoan, beraiek berriragarriko dira, baina agian ez dute mezurik jasoko bitartean.</string> + <string name="purge_key">Gakoa purgatu</string> + <string name="purge_key_desc_part1">Ziur al zaude gako hau purgatu nahi duzulaz?</string> + <string name="purge_key_desc_part2">Modu itzulezinean arriskutsutzat hartuko da, eta ezingo duzu gakoarekin berriro saio berri bat sortu.</string> <string name="fetching_history_from_server">Mezuak zerbitzaritik eskuratzen</string> <string name="no_more_history_on_server">Mezu gehiagorik ez zerbitzarian</string> <string name="updating">Eguneratzen...</string> @@ -427,7 +445,6 @@ <string name="received_location">Kokapena jaso da</string> <string name="title_undo_swipe_out_conversation">Elkarrizketa itxi egin da</string> <string name="title_undo_swipe_out_muc">Konferentzia utzi egin da</string> - <string name="pref_certificate_options">Ziurtagirien aukerak</string> <string name="pref_dont_trust_system_cas_title">Sistemaren CAtaz ez fidatu</string> <string name="pref_dont_trust_system_cas_summary">Ziurtagiri guztiak eskuz onartu behar dira</string> <string name="pref_remove_trusted_certificates_title">Ziurtagiriak kendu</string> @@ -449,6 +466,13 @@ <string name="none">Bat ere ez</string> <string name="recently_used">Azkenengo aldiz erabilitakoa</string> <string name="choose_quick_action">Ekintza azkarra aukeratu</string> - <string name="file_not_found_on_remote_host">Fitxategia ez da aurkitu urruneko zerbitzarian</string> <string name="search_for_contacts_or_groups">Kontaktuak edo taldeak bilatu</string> + <string name="send_private_message">Mezu pribatua bidali</string> + <string name="user_has_left_conference">%s(e)k konferentzia utzi egin du</string> + <string name="username">Erabiltzaile izena</string> + <string name="username_hint">Erabiltzaile izena</string> + <string name="invalid_username">Hau ez da erabiltzaile izen baliodun bat</string> + <string name="download_failed_server_not_found">Deskargak huts egin du: zerbitzaria ez da aurkitu</string> + <string name="download_failed_file_not_found">Deskargak huts egin du: fitxategia ez da aurkitu</string> + <string name="download_failed_could_not_connect">Deskargak huts egin du: ezin izan da ostalarira konektatu</string> </resources> diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 0a42b56a..cfd79136 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Vibrer lors de l\'arrivée d\'un message</string> <string name="pref_sound">Sonore</string> <string name="pref_sound_summary">Jouer une sonnerie lors de l\'arrivée d\'un message</string> - <string name="pref_conference_notifications">Notifications lors des conférences</string> - <string name="pref_conference_notifications_summary">Toujours notifier l\'arrivée d\'un message provenant d\'une conférence.</string> <string name="pref_notification_grace_period">Période sans notification</string> <string name="pref_notification_grace_period_summary">Désactiver momentanément les notifications après l\'arrivée d\'une copie carbone.</string> <string name="pref_advanced_options">Options avancées</string> diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index ff6c5eeb..aad7906b 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -69,8 +69,6 @@ <string name="pref_vibrate_summary">Treme cando chega unha novo mensaxe</string> <string name="pref_sound">Son</string> <string name="pref_sound_summary">Reproduce un ton ca notificación</string> - <string name="pref_conference_notifications">Notificacións de conferencia</string> - <string name="pref_conference_notifications_summary">Siempre notifica cuando chega unha mensaxe de conferencia e non solo cuando chega unha mensaxe destacada</string> <string name="pref_notification_grace_period">Notificacións Carbons</string> <string name="pref_notification_grace_period_summary">Deshabilita as notificacións durante un corto periodo de tiempo despois de recibir a copia da mensaxe carbón</string> <string name="pref_advanced_options">Opcións avanzadas</string> diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index 6f2d967f..f859bde1 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Juga aktifkan getaran bila pesan baru tiba</string> <string name="pref_sound">Suara</string> <string name="pref_sound_summary">mainkan suara saat menerima notifikasi</string> - <string name="pref_conference_notifications">Notifikasi Conference</string> - <string name="pref_conference_notifications_summary">Selalu memberitahukan bila pesan conference baru diterima daripada hanya dicetak tebal</string> <string name="pref_notification_grace_period">Tenggang waktu pemberitahuan</string> <string name="pref_notification_grace_period_summary">Nonaktifkan pemberitahuan untuk waktu yang singkat setelah salinan diterima</string> <string name="pref_advanced_options">Opsi Lanjutan</string> @@ -424,7 +422,6 @@ <string name="received_location">Lokasi yang diterima</string> <string name="title_undo_swipe_out_conversation">Percakapan tertutup</string> <string name="title_undo_swipe_out_muc">Tinggalkan conference</string> - <string name="pref_certificate_options">Opsi Sertifikat</string> <string name="pref_dont_trust_system_cas_title">Jangan percaya sistem CA</string> <string name="pref_dont_trust_system_cas_summary">Semua sertifikat harus disetujui secara manual</string> <string name="pref_remove_trusted_certificates_title">Hapus sertifikat</string> diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 8397b043..4e61aca7 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -60,7 +60,7 @@ <string name="crash_report_title">Errore di Conversations</string> <string name="crash_report_message">Se scegli di inviare una segnalazione dell’errore aiuterai lo sviluppo di Conversations\n<b>Attenzione:</b> Questo utilizzerà il tuo account XMPP per inviare la segnalazione agli sviluppatori.</string> <string name="send_now">Invia adesso</string> - <string name="send_never">Non chiedere mai più</string> + <string name="send_never">Non chiedere più</string> <string name="problem_connecting_to_account">Impossibile collegarsi tramite questo utente</string> <string name="problem_connecting_to_accounts">Impossibile collegarsi tramite più utenti</string> <string name="touch_to_fix">Tocca qui per gestire i tuoi utenti</string> @@ -97,7 +97,7 @@ <string name="pref_xmpp_resource">Risorsa XMPP</string> <string name="pref_xmpp_resource_summary">Il nome con il quale questo client si identifica</string> <string name="pref_accept_files">Accetta i file</string> - <string name="pref_accept_files_size_summary">Accetta automaticamente i file più piccoli di…</string> + <string name="pref_accept_files_summary">Accetta automaticamente i file più piccoli di…</string> <string name="pref_notification_settings">Impostazioni di Notifica</string> <string name="pref_notifications">Notifiche</string> <string name="pref_notifications_summary">Notifica quando arriva un nuovo messaggio</string> @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Vibra anche quando arriva un nuovo messaggio</string> <string name="pref_sound">Suono</string> <string name="pref_sound_summary">Riproduci una suoneria con la notifica</string> - <string name="pref_conference_notifications">Notifiche Conferenze</string> - <string name="pref_conference_notifications_summary">Notifica sempre quando arriva un nuovo messaggio da una conferenza, invece che solo quando in primo piano</string> <string name="pref_notification_grace_period">Periodo tra notifiche</string> <string name="pref_notification_grace_period_summary">Disabilita le notifiche per un breve lasso di tempo dopo che un messaggio è stato ricevuto</string> <string name="pref_advanced_options">Opzioni Avanzate</string> @@ -293,6 +291,7 @@ <string name="copy_text">Copia testo</string> <string name="copy_original_url">Copia URL originale</string> <string name="send_again">Invia di nuovo</string> + <string name="file_url">URL del file</string> <string name="message_text">Messaggio di testo</string> <string name="url_copied_to_clipboard">URL copiato</string> <string name="message_copied_to_clipboard">Messaggio copiato</string> @@ -406,6 +405,7 @@ <string name="apk">Applicazione Android</string> <string name="vcard">Contatto</string> <string name="received_x_file">Ricevuto %s</string> + <string name="disable_foreground_service">Disabilita i servizi in background</string> <string name="touch_to_open_conversations">Tocca per avviare Conversations</string> <string name="avatar_has_been_published">Il tuo avatar è stato pubblicato!</string> <string name="sending_x_file">Invio %s</string> @@ -422,7 +422,6 @@ <string name="received_location">Posizione ricevuta</string> <string name="title_undo_swipe_out_conversation">Conversazione interrotta</string> <string name="title_undo_swipe_out_muc">Conferenza terminata</string> - <string name="pref_certificate_options">Opzioni per i certificati</string> <string name="pref_dont_trust_system_cas_title">Non ti fidare delle CA di sistema</string> <string name="pref_dont_trust_system_cas_summary">Tutti i certificati devono essere accettati manualmente</string> <string name="pref_remove_trusted_certificates_title">Elimina i certificati</string> @@ -439,4 +438,7 @@ <item quantity="one">Seleziona il %d contatto</item> <item quantity="other">Selezionati %d contatti</item> </plurals> + <string name="none">Nessuno</string> + <string name="recently_used">Usati recentemente</string> + <string name="search_for_contacts_or_groups">Cerca contatti o gruppi</string> </resources> diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index cc12c7d0..3b62cd5e 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -90,8 +90,6 @@ <string name="pref_vibrate_summary">הרטט גם כאשר הודעה חדשה מגיעה</string> <string name="pref_sound">צליל</string> <string name="pref_sound_summary">נגן צלצול עם התראה</string> - <string name="pref_conference_notifications">התראות ועידה</string> - <string name="pref_conference_notifications_summary">תמיד תודיע כאשר הודעת ועידה חדשה מגיעה במקום רק כאשר מודגשת</string> <string name="pref_notification_grace_period">משך ארכת התראה</string> <string name="pref_notification_grace_period_summary">נטרל התראות לזמן קצר לאחר שהודעת פחם התקבלה</string> <string name="pref_advanced_options">אפשרויות מתקדמות</string> diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 35347734..aae2e48d 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -105,8 +105,8 @@ <string name="pref_vibrate_summary">新しいメッセージが到着したときに振動もします</string> <string name="pref_sound">サウンド</string> <string name="pref_sound_summary">通知で着信音を再生します</string> - <string name="pref_conference_notifications">会議通知</string> - <string name="pref_conference_notifications_summary">新しい会議メッセージが到着したとき、ハイライト表示ではなく、常に通知します</string> + <string name="pref_conference_notifications">公開会議で通知</string> + <string name="pref_conference_notifications_summary">強調表示されたときだけではなく、公開会議にメッセージが到着したときにいつも通知します</string> <string name="pref_notification_grace_period">通知猶予期間</string> <string name="pref_notification_grace_period_summary">カーボンコピーを受信した後、短時間、通知を無効にします</string> <string name="pref_advanced_options">詳細オプション</string> @@ -152,6 +152,7 @@ <string name="encryption_choice_none">プレーンテキスト</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> <string name="mgmt_account_edit">アカウントの編集</string> <string name="mgmt_account_delete">アカウントを削除</string> <string name="mgmt_account_disable">一時的に無効にする</string> @@ -204,6 +205,13 @@ <string name="reception_failed">受信に失敗しました</string> <string name="your_fingerprint">あなたのフィンガープリント</string> <string name="otr_fingerprint">OTR フィンガープリント</string> + <string name="omemo_fingerprint">OMEMO フィンガープリント</string> + <string name="omemo_fingerprint_selected_message">メッセージの OMEMO フィンガープリント</string> + <string name="this_device_omemo_fingerprint">自分の OMEMO フィンガープリント</string> + <string name="other_devices">他のデバイス</string> + <string name="trust_omemo_fingerprints">OMEMO フィンガープリントを信頼</string> + <string name="fetching_keys">鍵の取得中...</string> + <string name="done">完了</string> <string name="verify">検証</string> <string name="decrypt">復号化</string> <string name="conferences">会議</string> @@ -284,6 +292,7 @@ <string name="pref_conference_name">会議名</string> <string name="pref_conference_name_summary">会議を識別するために JID の代わりにルームのテーマを使用します</string> <string name="toast_message_otr_fingerprint">OTR フィンガープリントをクリップボードにコピーしました!</string> + <string name="toast_message_omemo_fingerprint">OMEMO フィンガープリントをクリップボードにコピーしました!</string> <string name="conference_banned">あなたはこの会議から禁止されています</string> <string name="conference_members_only">この会議はメンバーのみです</string> <string name="conference_kicked">あなたはこの会議からキックされました</string> @@ -349,7 +358,15 @@ <string name="secret_accepted">秘密を受取ました!</string> <string name="reset">リセット</string> <string name="account_image_description">アカウント アバター</string> - <string name="copy_otr_clipboard_description">OTR フィンガープリントをクリップボードにコピーしました</string> + <string name="copy_otr_clipboard_description">OTR フィンガープリントをクリップボードにコピー</string> + <string name="copy_omemo_clipboard_description">OMEMO フィンガープリントをクリップボードにコピー</string> + <string name="regenerate_omemo_key">OMEMO キーを再生成</string> + <string name="wipe_omemo_pep">PEP から他のデバイスを消去</string> + <string name="clear_other_devices">デバイスをクリア</string> + <string name="clear_other_devices_desc">OMEMO のアナウンスから他のすべてのデバイスをクリアしてもよろしいですか? お使いのデバイスが次回接続したとき、それらは自分自身を再アナウンスしますが、それらはその間に送信されたメッセージを受信できない場合があります。</string> + <string name="purge_key">鍵を消去</string> + <string name="purge_key_desc_part1">この鍵を消去してもよろしいですか?</string> + <string name="purge_key_desc_part2">これは不可逆的に侵害とみなされ、あなたは再びそのセッションを構築することはできません。</string> <string name="fetching_history_from_server">サーバーから履歴を取得中</string> <string name="no_more_history_on_server">サーバーにこれ以上履歴はありません</string> <string name="updating">アップデート中…</string> @@ -426,8 +443,7 @@ <string name="location">位置</string> <string name="received_location">位置を受信しました</string> <string name="title_undo_swipe_out_conversation">会話が閉じられました</string> - <string name="title_undo_swipe_out_muc">退出した会話</string> - <string name="pref_certificate_options">証明書オプション</string> + <string name="title_undo_swipe_out_muc">会議を退出しました</string> <string name="pref_dont_trust_system_cas_title">システムの CA を信頼しない</string> <string name="pref_dont_trust_system_cas_summary">すべての証明書を手動で承認する必要があります</string> <string name="pref_remove_trusted_certificates_title">証明書を削除</string> @@ -447,6 +463,13 @@ <string name="none">なし</string> <string name="recently_used">最近使用した</string> <string name="choose_quick_action">クイックアクションの選択</string> - <string name="file_not_found_on_remote_host">リモートサーバーにファイルが見つかりません</string> <string name="search_for_contacts_or_groups">連絡先またはグループの検索</string> + <string name="send_private_message">プライベートメッセージを送信</string> + <string name="user_has_left_conference">%s が会議を退出しました!</string> + <string name="username">ユーザー名</string> + <string name="username_hint">ユーザー名</string> + <string name="invalid_username">これは有効なユーザー名ではありません</string> + <string name="download_failed_server_not_found">ダウンロードに失敗しました: サーバーが見つかりません</string> + <string name="download_failed_file_not_found">ダウンロードに失敗しました: ファイルが見つかりません</string> + <string name="download_failed_could_not_connect">ダウンロードに失敗しました: ホストに接続できませんでした</string> </resources> diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index cdc3737a..a164dd10 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">새 메세지 도착시 진동 </string> <string name="pref_sound">소리 </string> <string name="pref_sound_summary">알림과 동시에 벨소리 재생 </string> - <string name="pref_conference_notifications">회의 알림 </string> - <string name="pref_conference_notifications_summary">새 회의 메세지가 도착시, 강조됐을 때 뿐만 아니라 항상 알림 </string> <string name="pref_notification_grace_period">알림 유예 </string> <string name="pref_notification_grace_period_summary">Carbon Copy 수신 후에 잠시동안 알림 해제</string> <string name="pref_advanced_options">추가 설정 </string> @@ -424,7 +422,6 @@ <string name="received_location">위치 수신 </string> <string name="title_undo_swipe_out_conversation">대화 끝남 </string> <string name="title_undo_swipe_out_muc">회의에서 나감 </string> - <string name="pref_certificate_options">인증 설정 </string> <string name="pref_dont_trust_system_cas_title">시스템 CA를 신뢰하지 않음 </string> <string name="pref_dont_trust_system_cas_summary">모든 인증서는 수동으로 승인되어야 함 </string> <string name="pref_remove_trusted_certificates_title">인증서 삭제 </string> diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 2873a793..d693f916 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -2,25 +2,25 @@ <resources> <string name="action_settings">Instellingen</string> <string name="action_add">Nieuw gesprek</string> - <string name="action_accounts">Beheer account</string> - <string name="action_end_conversation">Beëindig gesprek</string> + <string name="action_accounts">Accounts beheren</string> + <string name="action_end_conversation">Gesprek beëindigen</string> <string name="action_contact_details">Contactgegevens</string> <string name="action_muc_details">Gespreksgegevens</string> <string name="action_secure">Beveiligd gesprek</string> - <string name="action_add_account">Voeg account toe</string> - <string name="action_edit_contact">Verander naam</string> - <string name="action_add_phone_book">Voeg toe aan telefoonboek</string> - <string name="action_delete_contact">Verwijder uit lijst</string> - <string name="action_block_contact">Blokkeer contact</string> - <string name="action_unblock_contact">Deblokkeer contact</string> - <string name="action_block_domain">Blokkeer domein</string> - <string name="action_unblock_domain">Deblokkeer domein</string> - <string name="title_activity_manage_accounts">Beheer accounts</string> + <string name="action_add_account">Account toevoegen</string> + <string name="action_edit_contact">Naam veranderen</string> + <string name="action_add_phone_book">Toevoegen aan telefoonboek</string> + <string name="action_delete_contact">Verwijderen uit lijst</string> + <string name="action_block_contact">Contact blokkeren</string> + <string name="action_unblock_contact">Contact deblokkeren</string> + <string name="action_block_domain">Domein blokkeren</string> + <string name="action_unblock_domain">Domein deblokkeren</string> + <string name="title_activity_manage_accounts">Accounts beheren</string> <string name="title_activity_settings">Instellingen</string> <string name="title_activity_conference_details">Groepsgespreksgegevens</string> <string name="title_activity_contact_details">Contactgegevens</string> <string name="title_activity_sharewith">Delen met gesprek</string> - <string name="title_activity_start_conversation">Start gesprek</string> + <string name="title_activity_start_conversation">Gesprek starten</string> <string name="title_activity_choose_contact">Kies contact</string> <string name="title_activity_block_list">Geblokkeerde contacten</string> <string name="just_now">net</string> @@ -42,101 +42,102 @@ <string name="unblock_domain_text">Alle contacten van %s deblokkeren?</string> <string name="contact_blocked">Contact geblokkeerd</string> <string name="remove_bookmark_text">Wil je %s als bladwijzer verwijderen? Het gesprek met deze account zal niet worden verwijderd.</string> - <string name="register_account">Registreer nieuwe account op server</string> - <string name="change_password_on_server">Verander wachtwoord op server</string> - <string name="share_with">Deel met</string> - <string name="start_conversation">Start gesprek</string> - <string name="invite_contact">Nodig contact uit</string> + <string name="register_account">Nieuwe account op server registreren</string> + <string name="change_password_on_server">Wachtwoord op server veranderen</string> + <string name="share_with">Delen met…</string> + <string name="start_conversation">Gesprek starten</string> + <string name="invite_contact">Contact uitnodigen</string> <string name="contacts">Contacten</string> - <string name="cancel">Annuleer</string> - <string name="set">Stel in</string> - <string name="add">Voeg toe</string> - <string name="edit">Bewerk</string> - <string name="delete">Verwijder</string> - <string name="block">Blokkeer</string> - <string name="unblock">Deblokkeer</string> - <string name="save">Sla op</string> - <string name="ok">OK</string> + <string name="cancel">Annuleren</string> + <string name="set">Instellen</string> + <string name="add">Toevoegen</string> + <string name="edit">Bewerken</string> + <string name="delete">Verwijderen</string> + <string name="block">Blokkeren</string> + <string name="unblock">Deblokkeren</string> + <string name="save">Opslaan</string> + <string name="ok">Oké</string> <string name="crash_report_title">Conversations is gecrasht</string> - <string name="crash_report_message">Door het versturen van crash rapportages help je de ontwikkeling van Conversations.\n\n<b>Waarschuwing:</b> Deze app zal je XMPP account gebruiken om de crash rapportages te versturen naar de ontwikkelaars.</string> + <string name="crash_report_message">Door het versturen van crashrapportages help je de ontwikkeling van Conversations.\n\n<b>Waarschuwing:</b> Deze app zal je XMPP-account gebruiken om de crashrapportages te versturen naar de ontwikkelaars.</string> <string name="send_now">Nu versturen</string> <string name="send_never">Niet opnieuw vragen</string> <string name="problem_connecting_to_account">Account verbinden mislukt</string> <string name="problem_connecting_to_accounts">Verbinden met meerdere accounts mislukt</string> <string name="touch_to_fix">Raak hier aan om accounts te beheren</string> - <string name="attach_file">Voeg bestand bij</string> - <string name="not_in_roster">Het contact is geen onderdeel van uw lijst. Wil je het toevoegen?</string> - <string name="add_contact">Voeg contact toe</string> + <string name="attach_file">Bestand bijvoegen</string> + <string name="not_in_roster">Het contact is geen onderdeel van je lijst. Wil je hem/haar toevoegen?</string> + <string name="add_contact">Contact toevoegen</string> <string name="send_failed">afleveren mislukt</string> <string name="send_rejected">geweigerd</string> <string name="preparing_image">Bezig met voorbereiden van versturen van afbeelding</string> - <string name="action_clear_history">Wis geschiedenis</string> - <string name="clear_conversation_history">Wis gespreksgeschiedenis</string> + <string name="action_clear_history">Geschiedenis wissen</string> + <string name="clear_conversation_history">Gespreksgeschiedenis wissen</string> <string name="clear_histor_msg">Wil je alle berichten in dit gesprek verwijderen?\n\n<b>Waarschuwing:</b> Dit zal geen invloed hebben op de berichten opgeslagen op andere apparaten of servers.</string> - <string name="delete_messages">Verwijder berichten</string> + <string name="delete_messages">Berichten verwijderen</string> <string name="also_end_conversation">Beëindig dit gesprek na afloop</string> <string name="choose_presence">Kies aanwezigheid om te tonen aan contact</string> - <string name="send_plain_text_message">Verstuur eenvoudig tekst bericht</string> - <string name="send_otr_message">Verstuur OTR versleuteld bericht</string> - <string name="send_pgp_message">Verstuur OpenPGP versleuteld bericht</string> + <string name="send_plain_text_message">Verstuur onversleuteld bericht</string> + <string name="send_otr_message">Verstuur OTR-versleuteld bericht</string> + <string name="send_omemo_message">Verstuur OMEMO-versleuteld bericht</string> + <string name="send_pgp_message">Verstuur OpenPGP-versleuteld bericht</string> <string name="your_nick_has_been_changed">Je naam is veranderd</string> <string name="send_unencrypted">Verstuur onversleuteld</string> <string name="decryption_failed">Ontsleutelen mislukt. Misschien heb je niet de juiste private sleutel.</string> <string name="openkeychain_required">OpenKeychain</string> <string name="openkeychain_required_long">Conversations gebruikt een derde partij app genaamd <b>OpenKeychain</b> om berichten te versleutelen en ontsleutelen, en om publieke sleutels te beheren.\n\nOpenKeychain is beschikbaar onder de GPLv3 en beschikbaar op F-Droid en Google Play.\n\n<small>(Herstart Conversations na installatie.)</small></string> - <string name="restart">Herstart</string> - <string name="install">Installeer</string> - <string name="offering">offering…</string> + <string name="restart">Herstarten</string> + <string name="install">Installeren</string> + <string name="offering">bezig met aanbieden…</string> <string name="waiting">wachten…</string> - <string name="no_pgp_key">Geen OpenPGP sleutel gevonden</string> + <string name="no_pgp_key">Geen OpenPGP-sleutel gevonden</string> <string name="contact_has_no_pgp_key">Conversations kan je berichten niet versleutelen omdat je contact geen publieke sleutel heeft ingesteld.\n\n<small>Vraag je contact om OpenPGP te configureren.</small></string> - <string name="no_pgp_keys">Geen OpenPGP sleutels gevonden</string> + <string name="no_pgp_keys">Geen OpenPGP-sleutels gevonden</string> <string name="contacts_have_no_pgp_keys">Conversations kan je berichten niet versleutelen omdat je contacten geen publieke sleutel hebben ingesteld.\n\n<small>Vraag je contacten om OpenPGP te configureren.</small></string> <string name="encrypted_message_received"><i>Versleuteld bericht ontvangen. Raak aan om te bekijken en te ontsleutelen.</i></string> <string name="pref_general">Algemeen</string> - <string name="pref_xmpp_resource">XMPP resource</string> - <string name="pref_xmpp_resource_summary">De naam waarmee deze client zich identificeert</string> - <string name="pref_accept_files">Accepteer bestanden</string> - <string name="pref_accept_files_size_summary">Accepteer automatisch bestanden kleiner dan…</string> + <string name="pref_xmpp_resource">XMPP-bron</string> + <string name="pref_xmpp_resource_summary">De naam waarmee deze cliënt zich identificeert</string> + <string name="pref_accept_files">Aanvaard bestanden</string> + <string name="pref_accept_files_summary">Aanvaard automatisch bestanden kleiner dan…</string> <string name="pref_notification_settings">Meldingsinstellingen</string> <string name="pref_notifications">Meldingen</string> <string name="pref_notifications_summary">Melding als een nieuw bericht arriveert</string> <string name="pref_vibrate">Trillen</string> <string name="pref_vibrate_summary">Tril ook wanneer een nieuw bericht arriveert</string> <string name="pref_sound">Geluid</string> - <string name="pref_sound_summary">Speel ringtone af bij melding</string> - <string name="pref_conference_notifications">Groepsgespreksmeldingen</string> - <string name="pref_conference_notifications_summary">Toon altijd meldingen als er nieuwe berichten arriveren in groepsgesprekken in plaats van alleen wanneer gemarkeerd</string> + <string name="pref_sound_summary">Speel beltoon af bij melding</string> + <string name="pref_conference_notifications">Meldingen bij groepsgespreken</string> + <string name="pref_conference_notifications_summary">Laat altijd een melding zien wanneer er berichten zijn ontvangen in groepsgesprekken in plaats van alleen bij het noemen van je naam</string> <string name="pref_notification_grace_period">Uitstelperiode voor meldingen</string> - <string name="pref_notification_grace_period_summary">Zet meldingen voor korte tijd uit als er een carbon copy wordt ontvangen</string> + <string name="pref_notification_grace_period_summary">Schakel meldingen voor korte tijd uit als er een carbon copy wordt ontvangen</string> <string name="pref_advanced_options">Geavanceerde instellingen</string> - <string name="pref_never_send_crash">Verstuur nooit crash rapportages</string> - <string name="pref_never_send_crash_summary">Door crash rapportages te versturen help je de ontwikkeling van Conversations</string> + <string name="pref_never_send_crash">Verstuur nooit crashrapportages</string> + <string name="pref_never_send_crash_summary">Door crashrapportages te versturen help je de ontwikkeling van Conversations</string> <string name="pref_confirm_messages">Bevestig berichten</string> <string name="pref_confirm_messages_summary">Laat je contacten weten wanneer je berichten hebt ontvangen en gelezen</string> - <string name="pref_ui_options">UI opties</string> + <string name="pref_ui_options">UI-opties</string> <string name="openpgp_error">OpenKeychain rapporteerde een fout</string> - <string name="error_decrypting_file">I/O fout tijdens ontsleutelen bestand</string> - <string name="accept">Aanvaard</string> + <string name="error_decrypting_file">I/O-fout tijdens ontsleutelen van bestand</string> + <string name="accept">Aanvaarden</string> <string name="error">Er is een fout opgetreden</string> <string name="pref_grant_presence_updates">Verleen toestemming voor aanwezigheidsupdates</string> - <string name="pref_grant_presence_updates_summary">Op voorhand toestemming verlenen en vragen aan contacten die je hebt aangemaakt</string> + <string name="pref_grant_presence_updates_summary">Op voorhand toestemming voor aanwezigheidsabonnementen verlenen en vragen aan contacten die je hebt aangemaakt</string> <string name="subscriptions">Abonnementen</string> <string name="your_account">Je account</string> <string name="keys">Sleutels</string> <string name="send_presence_updates">Verstuur aanwezigheidsupdates</string> <string name="receive_presence_updates">Ontvang aanwezigheidsupdates</string> <string name="ask_for_presence_updates">Vraag naar aanwezigheidsupdates</string> - <string name="attach_choose_picture">Kies afbeelding</string> - <string name="attach_take_picture">Neem foto</string> + <string name="attach_choose_picture">Afbeelding kiezen</string> + <string name="attach_take_picture">Foto nemen</string> <string name="preemptively_grant">Op voorhand toestemming verlenen voor abonneren</string> <string name="error_not_an_image_file">Het bestand dat je gekozen hebt is geen afbeelding</string> <string name="error_compressing_image">Fout tijdens converteren van afbeelding</string> <string name="error_file_not_found">Bestand niet gevonden</string> - <string name="error_io_exception">Algemene I/O fout. Misschien is er geen opslagruimte meer beschikbaar?</string> - <string name="error_security_exception_during_image_copy">De app die je gebruikte om de afbeelding te selecteren heeft niet voldoende toegang geleverd om het bestand te lezen.\n\n<small>Gebruik een andere app om een afbeelding te kiezen</small></string> + <string name="error_io_exception">Algemene I/O-fout. Misschien is er geen opslagruimte meer beschikbaar?</string> + <string name="error_security_exception_during_image_copy">De app die je gebruikte om de afbeelding te selecteren heeft niet voldoende toegang geleverd om het bestand te lezen.\n\n<small>Gebruik een andere bestandsbeheerder om een afbeelding te kiezen</small></string> <string name="account_status_unknown">Onbekend</string> - <string name="account_status_disabled">Tijdelijk uitgezet</string> + <string name="account_status_disabled">Tijdelijk uitgeschakeld</string> <string name="account_status_online">Online</string> <string name="account_status_connecting">Verbinden\u2026</string> <string name="account_status_offline">Offline</string> @@ -144,31 +145,32 @@ <string name="account_status_not_found">Server niet gevonden</string> <string name="account_status_no_internet">Geen verbinding</string> <string name="account_status_regis_fail">Registratie mislukt</string> - <string name="account_status_regis_conflict">Gebruikersnaam bezet</string> - <string name="account_status_regis_success">Registratie compleet</string> + <string name="account_status_regis_conflict">Gebruikersnaam is al in gebruik</string> + <string name="account_status_regis_success">Registratie voltooid</string> <string name="account_status_regis_not_sup">Server ondersteunt geen registratie</string> <string name="account_status_security_error">Fout bij beveiliging</string> <string name="account_status_incompatible_server">Incompatibele server</string> <string name="encryption_choice_none">Onversleuteld</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> - <string name="mgmt_account_edit">Bewerk account</string> - <string name="mgmt_account_delete">Verwijder</string> - <string name="mgmt_account_disable">Tijdelijk uitzetten</string> - <string name="mgmt_account_publish_avatar">Publish avatar</string> - <string name="mgmt_account_publish_pgp">Publish OpenPGP public key</string> - <string name="mgmt_account_enable">Aanzetten</string> + <string name="encryption_choice_omemo">OMEMO</string> + <string name="mgmt_account_edit">Account bewerken</string> + <string name="mgmt_account_delete">Account verwijderen</string> + <string name="mgmt_account_disable">Tijdelijk uitschakelen</string> + <string name="mgmt_account_publish_avatar">Avatar publiceren</string> + <string name="mgmt_account_publish_pgp">Publiceer OpenPGP publieke sleutel</string> + <string name="mgmt_account_enable">Account inschakelen</string> <string name="mgmt_account_are_you_sure">Ben je zeker?</string> <string name="mgmt_account_delete_confirm_text">Als je je account verwijdert wordt je volledige gespreksgeschiedenis gewist</string> - <string name="attach_record_voice">Neem stem op</string> - <string name="account_settings_jabber_id">Jabber ID:</string> + <string name="attach_record_voice">Stem opnemen</string> + <string name="account_settings_jabber_id">Jabber-ID:</string> <string name="account_settings_password">Wachtwoord:</string> <string name="account_settings_example_jabber_id">gebruikersnaam@voorbeeld.nl</string> - <string name="account_settings_confirm_password">Bevestig wachtwoord:</string> + <string name="account_settings_confirm_password">Wachtwoord bevestigen</string> <string name="password">Wachtwoord</string> - <string name="confirm_password">Bevestig wachtwoord</string> + <string name="confirm_password">Wachtwoord bevestigen</string> <string name="passwords_do_not_match">Wachtwoorden komen niet overeen</string> - <string name="invalid_jid">Dit is geen geldig Jabber ID</string> + <string name="invalid_jid">Dit is geen geldige Jabber-ID</string> <string name="error_out_of_memory">Geen geheugen beschikbaar. Afbeelding is te groot</string> <string name="add_phone_book_text">Wil je %s toevoegen aan de contactenlijst op je telefoon?</string> <string name="contact_status_online">online</string> @@ -179,7 +181,7 @@ <string name="contact_status_offline">offline</string> <string name="muc_details_conference">Groepsgesprek</string> <string name="muc_details_other_members">Andere leden</string> - <string name="server_info_show_more">Server info</string> + <string name="server_info_show_more">Server-info</string> <string name="server_info_mam">XEP-0313: MAM</string> <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> <string name="server_info_csi">XEP-0352: Client State Indication</string> @@ -189,7 +191,7 @@ <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> <string name="server_info_available">beschikbaar</string> <string name="server_info_unavailable">niet beschikbaar</string> - <string name="missing_public_keys">Ontbrekende publieke sleutel aankondigingen</string> + <string name="missing_public_keys">Ontbrekende publieke sleutel-aankondigingen</string> <string name="last_seen_now">zonet voor het laatst gezien</string> <string name="last_seen_min">1 minuut geleden voor het laatst gezien</string> <string name="last_seen_mins">%d minuten geleden voor het laatst gezien</string> @@ -199,21 +201,28 @@ <string name="last_seen_days">%d dagen geleden voor het laatst gezien</string> <string name="never_seen">nog nooit gezien</string> <string name="install_openkeychain">Versleuteld bericht. Installeer OpenKeychain om te ontsleutelen.</string> - <string name="unknown_otr_fingerprint">Onbekende OTR vingerafdruk</string> + <string name="unknown_otr_fingerprint">Onbekende OTR-vingerafdruk</string> <string name="openpgp_messages_found">OpenPGP-versleutelde berichten gevonden</string> <string name="reception_failed">Ontvangen mislukt</string> <string name="your_fingerprint">Jouw vingerafdruk</string> - <string name="otr_fingerprint">OTR vingerafdruk</string> - <string name="verify">Bevestig</string> - <string name="decrypt">Ontsleutel</string> + <string name="otr_fingerprint">OTR-vingerafdruk</string> + <string name="omemo_fingerprint">OMEMO-vingerafdruk</string> + <string name="omemo_fingerprint_selected_message">OMEMO-vingerafdruk van bericht</string> + <string name="this_device_omemo_fingerprint">Eigen OMEMO-vingerafdruk</string> + <string name="other_devices">Andere apparaten</string> + <string name="trust_omemo_fingerprints">Vertrouw OMEMO-vingerafdrukken</string> + <string name="fetching_keys">Ophalen van de sleutels...</string> + <string name="done">Klaar</string> + <string name="verify">Bevestigen</string> + <string name="decrypt">Ontsleutelen</string> <string name="conferences">Groepsgesprekken</string> <string name="search">Zoeken</string> - <string name="create_contact">Maak contact aan</string> + <string name="create_contact">Contact aanmaken</string> <string name="join_conference">Aan groepsgesprek deelnemen</string> - <string name="delete_contact">Verwijder contact</string> - <string name="view_contact_details">Bekijk contactgegevens</string> - <string name="block_contact">Blokkeer contact</string> - <string name="unblock_contact">Deblokkeer contact</string> + <string name="delete_contact">Contact verwijderen</string> + <string name="view_contact_details">Contactgegevens bekijken</string> + <string name="block_contact">Contact blokkeren</string> + <string name="unblock_contact">Contact deblokkeren</string> <string name="create">Aanmaken</string> <string name="contact_already_exists">Het contact bestaat al</string> <string name="join">Deelnemen</string> @@ -223,15 +232,15 @@ <string name="delete_bookmark">Bladwijzer verwijderen</string> <string name="bookmark_already_exists">Deze bladwijzer bestaat al</string> <string name="you">Jij</string> - <string name="action_edit_subject">Onderwerp groepsgesprek bewerken</string> + <string name="action_edit_subject">Onderwerp van groepsgesprek bewerken</string> <string name="conference_not_found">Groepsgesprek niet gevonden</string> <string name="leave">Verlaten</string> <string name="contact_added_you">Contact heeft je toegevoegd aan zijn/haar contacten</string> <string name="add_back">Contact toevoegen aan eigen contacten</string> <string name="contact_has_read_up_to_this_point">%s heeft tot hier gelezen</string> - <string name="publish">Publiceer</string> + <string name="publish">Publiceren</string> <string name="touch_to_choose_picture">Raak avatar aan om een foto uit de galerij te kiezen</string> - <string name="publish_avatar_explanation"><b>Aandacht:</b> Iedereen die je aanwezigheidsupdates ontvangt zal deze foto kunnen zien.</string> + <string name="publish_avatar_explanation">Let op: iedereen die je aanwezigheidsupdates ontvangt zal deze foto kunnen zien.</string> <string name="publishing">Publiceren…</string> <string name="error_publish_avatar_server_reject">De server weigerde de publicatie van je afbeelding</string> <string name="error_publish_avatar_converting">Fout bij converteren van afbeelding</string> @@ -240,19 +249,19 @@ <string name="error_publish_avatar_no_server_support">Je server ondersteunt de publicatie van avatars niet</string> <string name="private_message">gefluisterd</string> <string name="private_message_to">naar %s</string> - <string name="send_private_message_to">Stuur privébericht naar %s</string> + <string name="send_private_message_to">Privébericht sturen naar %s</string> <string name="connect">Verbinden</string> <string name="account_already_exists">Deze account bestaat al</string> <string name="next">Volgende</string> <string name="server_info_session_established">Huidige sessie gevestigd</string> <string name="additional_information">Bijkomstige informatie</string> <string name="skip">Overslaan</string> - <string name="disable_notifications">Meldingen uitzetten</string> - <string name="disable_notifications_for_this_conversation">Meldingen uitzetten voor dit gesprek</string> - <string name="notifications_disabled">Meldingen zijn uitgezet</string> - <string name="enable">Aanzetten</string> + <string name="disable_notifications">Meldingen uitschakelen</string> + <string name="disable_notifications_for_this_conversation">Meldingen uitschakelen voor dit gesprek</string> + <string name="notifications_disabled">Meldingen zijn uitgeschakeld</string> + <string name="enable">Inschakelen</string> <string name="conference_requires_password">Wachtwoord nodig voor toegang tot groepsgesprek</string> - <string name="enter_password">Wachtwoord:</string> + <string name="enter_password">Wachtwoord invoeren</string> <string name="missing_presence_updates">Ontbrekende aanwezigheidsupdates van contact</string> <string name="request_presence_updates">Vraag eerst aanwezigheidsupdates van je contact aan.\n\n<small>Dit wordt gebruikt om te bepalen welke client(s) je contact gebruikt.</small></string> <string name="request_now">Nu aanvragen</string> @@ -261,10 +270,10 @@ <string name="ignore">Negeren</string> <string name="without_mutual_presence_updates"><b>Waarschuwing:</b> Dit verzenden zonder wederzijdse aanwezigheidsupdates kan voor onverwachte problemen zorgen.\n\n<small>Ga naar contactgegevens om je aanwezigheidsupdates te bevestigen.</small></string> <string name="pref_encryption_settings">Versleutelingsinstellingen</string> - <string name="pref_force_encryption">Verplicht end-to-end versleuteling</string> + <string name="pref_force_encryption">Verplicht end-to-end-versleuteling</string> <string name="pref_force_encryption_summary">Stuur berichten altijd versleuteld (behalve in groepsgesprekken)</string> <string name="pref_dont_save_encrypted">Sla versleutelde berichten niet op</string> - <string name="pref_dont_save_encrypted_summary"><b>Waarschuwing:</b> Dit kan leiden tot verlies van berichten</string> + <string name="pref_dont_save_encrypted_summary">Waarschuwing: dit kan leiden tot verlies van berichten</string> <string name="pref_expert_options">Expert-instellingen</string> <string name="pref_expert_options_summary">Wees voorzichtig met deze instellingen</string> <string name="title_activity_about">Over Conversations</string> @@ -272,7 +281,7 @@ <string name="title_pref_quiet_hours">Stille uren</string> <string name="title_pref_quiet_hours_start_time">Begintijd</string> <string name="title_pref_quiet_hours_end_time">Eindtijd</string> - <string name="title_pref_enable_quiet_hours">Stille uren aanzetten</string> + <string name="title_pref_enable_quiet_hours">Stille uren inschakelen</string> <string name="pref_quiet_hours_summary">Tijdens stille uren worden meldingen onderdrukt</string> <string name="pref_use_larger_font">Vergroot lettergrootte</string> <string name="pref_use_larger_font_summary">Gebruik grotere lettertypes over de hele app</string> @@ -283,27 +292,29 @@ <string name="pref_expert_options_other">Andere</string> <string name="pref_conference_name">Groepsgespreksnaam</string> <string name="pref_conference_name_summary">Gebruik onderwerp van kamer ipv JID om groepsgesprekken te identificeren</string> - <string name="toast_message_otr_fingerprint">OTR vingerafdruk naar klembord gekopieerd!</string> + <string name="toast_message_otr_fingerprint">OTR-vingerafdruk gekopieerd naar klembord!</string> + <string name="toast_message_omemo_fingerprint">OMEMO-vingerafdruk gekopieerd naar klembord!</string> <string name="conference_banned">Je bent verbannen uit dit groepsgesprek</string> <string name="conference_members_only">Dit groepsgesprek is enkel voor leden</string> <string name="conference_kicked">Je bent uit dit groepsgesprek geschopt</string> - <string name="using_account">account %s gebruiken</string> + <string name="using_account">met account %s</string> + <string name="checking_x">%s op HTTP-host nakijken</string> <string name="not_connected_try_again">Je bent niet verbonden. Probeer later opnieuw</string> - <string name="check_x_filesize">Bekijk bestandsgrootte van %s</string> + <string name="check_x_filesize">Bestandsgrootte van %s controleren</string> <string name="message_options">Berichtopties</string> - <string name="copy_text">Kopieer tekst</string> - <string name="copy_original_url">Kopieer oorspronkelijke URL</string> - <string name="send_again">Verstuur opnieuw</string> + <string name="copy_text">Tekst kopiëren</string> + <string name="copy_original_url">Oorspronkelijke URL kopiëren</string> + <string name="send_again">Opnieuw versturen</string> <string name="file_url">Bestands-URL</string> <string name="message_text">Berichttekst</string> <string name="url_copied_to_clipboard">URL gekopieerd naar klembord</string> <string name="message_copied_to_clipboard">Bericht gekopieerd naar klembord</string> <string name="image_transmission_failed">Versturen van afbeelding mislukt</string> - <string name="scan_qr_code">Scan QR code</string> - <string name="show_qr_code">Toon QR code</string> - <string name="show_block_list">Toon geblokkeerde contacten</string> + <string name="scan_qr_code">QR-code scannen</string> + <string name="show_qr_code">QR-code tonen</string> + <string name="show_block_list">Geblokkeerde contacten weergeven</string> <string name="account_details">Accountgegevens</string> - <string name="verify_otr">Bevestig OTR</string> + <string name="verify_otr">OTR bevestigen</string> <string name="remote_fingerprint">Externe vingerafdruk</string> <string name="scan">scan</string> <string name="or_touch_phones">(of raak gsm\'s aan)</string> @@ -321,17 +332,17 @@ <string name="smp_requested">Contact vraagt SMP-bevestiging</string> <string name="no_otr_session_found">Geen geldige OTR-sessie gevonden!</string> <string name="conversations_foreground_service">Conversations</string> - <string name="pref_keep_foreground_service">Hou service in voorgrond</string> + <string name="pref_keep_foreground_service">Dienst in voorgrond houden</string> <string name="pref_keep_foreground_service_summary">Belet het besturingssysteem van je verbinding te onderbreken</string> - <string name="choose_file">Kies bestand</string> + <string name="choose_file">Bestand kiezen</string> <string name="receiving_x_file">Ontvangen van %1$s (%2$d%% voltooid)</string> - <string name="download_x_file">Download %s</string> + <string name="download_x_file">%s downloaden</string> <string name="file">bestand</string> - <string name="open_x_file">Open %s</string> + <string name="open_x_file">%s openen</string> <string name="sending_file">versturen (%1$d%% voltooid)</string> <string name="preparing_file">Bestand klaarmaken voor versturen</string> <string name="x_file_offered_for_download">%s aangeboden om te downloaden</string> - <string name="cancel_transmission">Annuleer bestandsoverdracht</string> + <string name="cancel_transmission">Bestandsoverdracht annuleren</string> <string name="file_transmission_failed">bestandsoverdracht mislukt</string> <string name="file_deleted">Het bestand is verwijderd</string> <string name="no_application_found_to_open_file">Geen applicatie om bestand te openen</string> @@ -340,7 +351,7 @@ <string name="are_you_sure_verify_fingerprint">Ben je zeker dat je de OTR-vingerafdruk van je contact wil bevestigen?</string> <string name="pref_show_dynamic_tags">Toon dynamische tags</string> <string name="pref_show_dynamic_tags_summary">Toon enkel-lezen tags onder contacten</string> - <string name="enable_notifications">Meldingen aanzetten</string> + <string name="enable_notifications">Meldingen inschakelen</string> <string name="conference_with">Groepsgesprek aanmaken met…</string> <string name="no_conference_server_found">Geen groepsgespreksserver gevonden</string> <string name="conference_creation_failed">Aanmaken van groepsgesprek mislukt!</string> @@ -348,7 +359,15 @@ <string name="secret_accepted">Geheim aanvaard!</string> <string name="reset">Opnieuw instellen</string> <string name="account_image_description">Account-avatar</string> - <string name="copy_otr_clipboard_description">Kopieer OTR-vingerafdruk naar klembord</string> + <string name="copy_otr_clipboard_description">OTR-vingerafdruk kopiëren naar klembord</string> + <string name="copy_omemo_clipboard_description">OMEMO-vingerafdruk kopiëren naar klembord</string> + <string name="regenerate_omemo_key">OMEMO-sleutel opnieuw aanmaken</string> + <string name="wipe_omemo_pep">Andere apparaten van PEP verwijderen</string> + <string name="clear_other_devices">Apparaten wissen</string> + <string name="clear_other_devices_desc">Ben je zeker dat je alle andere apparaten van de OMEMO-aankondiging wil wissen? De volgende keer dat je apparaten verbinding maken zullen ze zich opnieuw aankondigen, maar zullen ze misschien niet de berichten ontvangen die intussen zijn verzonden.</string> + <string name="purge_key">Sleutel verwijderen</string> + <string name="purge_key_desc_part1">Ben je zeker dat je deze sleutel wil verwijderen?</string> + <string name="purge_key_desc_part2">Ze zal onherroepelijk beschouwd worden als gecompromitteerd en je zal er nooit meer een nieuwe sessie mee kunnen bouwen.</string> <string name="fetching_history_from_server">Geschiedenis van server halen</string> <string name="no_more_history_on_server">Geen verdere geschiedenis op server</string> <string name="updating">Bijwerken…</string> @@ -365,8 +384,8 @@ <string name="current_password">Huidig wachtwoord</string> <string name="new_password">Nieuw wachtwoord</string> <string name="password_should_not_be_empty">Wachtwoord zou niet leeg mogen zijn</string> - <string name="enable_all_accounts">Alle accounts aanzetten</string> - <string name="disable_all_accounts">Alle accounts uitzetten</string> + <string name="enable_all_accounts">Alle accounts inschakelen</string> + <string name="disable_all_accounts">Alle accounts uitschakelen</string> <string name="perform_action_with">Actie uitvoeren met</string> <string name="no_affiliation">Geen aansluiting</string> <string name="no_role">Geen rol</string> @@ -377,7 +396,7 @@ <string name="remove_membership">Lidmaatschap verwijderen</string> <string name="grant_admin_privileges">Administratorprivileges verlenen</string> <string name="remove_admin_privileges">Administratorprivileges verwijderen</string> - <string name="remove_from_room">Verwijderen uit kamer</string> + <string name="remove_from_room">Verwijderen uit groepsgesprek</string> <string name="could_not_change_affiliation">Kon aansluiting niet wijzigen</string> <string name="ban_from_conference">Verbannen uit groepsgesprek</string> <string name="removing_from_public_conference">Je probeert %s te verwijderen uit een publiek groepsgesprek. De enige manier om dat te doen is door hem/haar permanent te verbannen.</string> @@ -396,7 +415,7 @@ <string name="two_hours">2 uur</string> <string name="eight_hours">8 uur</string> <string name="until_further_notice">Voor onbepaalde duur</string> - <string name="pref_input_options">Input-opties</string> + <string name="pref_input_options">Invoer-opties</string> <string name="pref_enter_is_send">Enter is versturen</string> <string name="pref_enter_is_send_summary">Gebruik de enter-toets om berichten te versturen</string> <string name="pref_display_enter_key">Toon enter-toets</string> @@ -408,33 +427,32 @@ <string name="apk">Android-applicatie</string> <string name="vcard">Contact</string> <string name="received_x_file">%s ontvangen</string> - <string name="disable_foreground_service">Voorgrond-service uitzetten</string> + <string name="disable_foreground_service">Voorgronddienst uitschakelen</string> <string name="touch_to_open_conversations">Raak aan om Conversations te openen</string> <string name="avatar_has_been_published">Avatar is gepubliceerd!</string> <string name="sending_x_file">Bezig met versturen van %s</string> <string name="offering_x_file">Bezig met aanbieden van %s</string> - <string name="hide_offline">Offline verbergen</string> - <string name="disable_account">Account uitzetten</string> + <string name="hide_offline">Offline contacten verbergen</string> + <string name="disable_account">Account uitschakelen</string> <string name="contact_is_typing">%s is aan het typen...</string> <string name="contact_has_stopped_typing">%s is gestopt met typen</string> - <string name="pref_chat_states">Type-meldingen</string> + <string name="pref_chat_states">Aan-het-typen-meldingen</string> <string name="pref_chat_states_summary">Laat je contacten weten wanneer je een nieuw bericht aan het schrijven bent</string> <string name="send_location">Locatie versturen</string> <string name="show_location">Locatie weergeven</string> - <string name="no_application_found_to_display_location">Geen applicatie gevonden om locatie weer te geven</string> + <string name="no_application_found_to_display_location">Geen applicatie om locatie weer te geven</string> <string name="location">Locatie</string> <string name="received_location">Locatie ontvangen</string> <string name="title_undo_swipe_out_conversation">Gesprek gesloten</string> <string name="title_undo_swipe_out_muc">Groepsgesprek verlaten</string> - <string name="pref_certificate_options">Certificaatopties</string> - <string name="pref_dont_trust_system_cas_title">Vertrouw geen systeem-CA\'s.</string> + <string name="pref_dont_trust_system_cas_title">Systeem-CA\'s niet vertrouwen</string> <string name="pref_dont_trust_system_cas_summary">Alle certificaten moeten handmatig goedgekeurd worden</string> - <string name="pref_remove_trusted_certificates_title">Verwijder certificaten</string> - <string name="pref_remove_trusted_certificates_summary">Verwijder handmatig goedgekeurde certificaten</string> + <string name="pref_remove_trusted_certificates_title">Certificaten verwijderen</string> + <string name="pref_remove_trusted_certificates_summary">Handmatig goedgekeurde certificaten verwijderen</string> <string name="toast_no_trusted_certs">Geen handmatig goedgekeurde certificaten</string> - <string name="dialog_manage_certs_title">Verwijder certificaten</string> - <string name="dialog_manage_certs_positivebutton">Verwijder selectie</string> - <string name="dialog_manage_certs_negativebutton">Annuleer</string> + <string name="dialog_manage_certs_title">Certificaten verwijderen</string> + <string name="dialog_manage_certs_positivebutton">Selectie verwijderen</string> + <string name="dialog_manage_certs_negativebutton">Annuleren</string> <plurals name="toast_delete_certificates"> <item quantity="one">%d certificaat verwijderd</item> <item quantity="other">%d certificaten verwijderd</item> @@ -447,6 +465,14 @@ <string name="pref_quick_action">Snelle actie</string> <string name="none">Geen</string> <string name="recently_used">Recent gebruikt</string> - <string name="choose_quick_action">Kies snelle actie</string> - <string name="file_not_found_on_remote_host">Bestand niet gevonden op externe server</string> + <string name="choose_quick_action">Snelle actie kiezen</string> + <string name="search_for_contacts_or_groups">Zoeken naar contacten of groepen</string> + <string name="send_private_message">Privébericht sturen</string> + <string name="user_has_left_conference">%s heeft het groepsgesprek verlaten!</string> + <string name="username">Gebruikersnaam</string> + <string name="username_hint">Gebruikersnaam</string> + <string name="invalid_username">Dit is geen geldige gebruikersnaam</string> + <string name="download_failed_server_not_found">Downloaden mislukt: server niet gevonden</string> + <string name="download_failed_file_not_found">Downloaden mislukt: bestand niet gevonden</string> + <string name="download_failed_could_not_connect">Downloaden mislukt: kon geen verbinding maken met host</string> </resources> diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index ea2ecc52..1ace55a8 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Wibruj, gdy nadejdzie wiadomość</string> <string name="pref_sound">Dźwięk</string> <string name="pref_sound_summary">Odtwórz dźwięk z powiadomieniem</string> - <string name="pref_conference_notifications">Powiadomienia konferencji</string> - <string name="pref_conference_notifications_summary">Zawsze powiadamiaj o nowej wiadomości w konferencji</string> <string name="pref_notification_grace_period">Opóźnienie powiadomień</string> <string name="pref_notification_grace_period_summary">Wyłącz powiadomienia przez krótki czas po otrzymaniu kopii wiadomości</string> <string name="pref_advanced_options">Opcje zaawansowane</string> @@ -204,6 +202,7 @@ <string name="reception_failed">Odbiór nieudany</string> <string name="your_fingerprint">Twój odcisk klucza</string> <string name="otr_fingerprint">Odcisk klucza OTR</string> + <string name="other_devices">Pozostałe urządzenia</string> <string name="verify">Weryfikuj</string> <string name="decrypt">Odszyfruj</string> <string name="conferences">Konferencje</string> @@ -424,7 +423,6 @@ <string name="received_location">Otrzymano lokalizację</string> <string name="title_undo_swipe_out_conversation">Zamknięto konwersację</string> <string name="title_undo_swipe_out_muc">Opuszczono konferencję</string> - <string name="pref_certificate_options">Ustawienia certyfikatów</string> <string name="pref_dont_trust_system_cas_title">Nie ufaj certyfikatom systemowym</string> <string name="pref_dont_trust_system_cas_summary">Wymagaj ręcznego potwierdzania certyfikatów</string> <string name="pref_remove_trusted_certificates_title">Usuń certyfikat</string> @@ -448,4 +446,5 @@ <string name="none">Brak</string> <string name="recently_used">Ostatnio używana</string> <string name="choose_quick_action">Wybierz szybką akcję</string> + <string name="user_has_left_conference">%s opuścił(a) konferencję!</string> </resources> diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index 559ae6cf..041b5e2d 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -99,8 +99,6 @@ <string name="pref_vibrate_summary">Vibrar também quando uma nova mensagem for recebida</string> <string name="pref_sound">Som</string> <string name="pref_sound_summary">Tocar um som com a notificação</string> - <string name="pref_conference_notifications">Notificações de conferência</string> - <string name="pref_conference_notifications_summary">Sempre notificar quando uma nova mensagem de conferencia for recebida ao invés de apenas quando a mesma for ressaltada</string> <string name="pref_notification_grace_period">Período de carência da notificação</string> <string name="pref_notification_grace_period_summary">Desativar notificações por um curto período após a copia oculta ser recebida</string> <string name="pref_advanced_options">Opções avançadas</string> diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 3a2515b7..053a675c 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -97,7 +97,7 @@ <string name="pref_xmpp_resource">Название ресурса</string> <string name="pref_xmpp_resource_summary">Имя которым Conversations идентифицирует себя</string> <string name="pref_accept_files">Принимать файлы</string> - <string name="pref_accept_files_size_summary">Автоматический прием файлов…</string> + <string name="pref_accept_files_summary">Автоматический прием файлов…</string> <string name="pref_notification_settings">Настройки Уведомлений</string> <string name="pref_notifications">Уведомление</string> <string name="pref_notifications_summary">Использовать звуковое уведомление когда приходят новые сообщения</string> @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Использовать вибрацию когда приходят новые сообщения</string> <string name="pref_sound">Звуковой сигнал</string> <string name="pref_sound_summary">Выберите звуковой сигнал для сообщений</string> - <string name="pref_conference_notifications">Уведомления конференции</string> - <string name="pref_conference_notifications_summary">Всегда сообщать при получении нового сообщения в конференции</string> <string name="pref_notification_grace_period">Отсрочка уведомлений</string> <string name="pref_notification_grace_period_summary">Не использовать уведомления, если вы прочитали сообщение на другом устройстве</string> <string name="pref_advanced_options">Дополнительные параметры</string> @@ -424,7 +422,6 @@ <string name="received_location">Получено местоположение</string> <string name="title_undo_swipe_out_conversation">Беседа окончена</string> <string name="title_undo_swipe_out_muc">Покинул беседу</string> - <string name="pref_certificate_options">Опции сертификата</string> <string name="pref_dont_trust_system_cas_title">Не доверять системным УЦ</string> <string name="pref_dont_trust_system_cas_summary">Все сертификаты должны быть подтверждены вручную</string> <string name="pref_remove_trusted_certificates_title">Удалить сертификат</string> diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 01211ad2..5bcc1aa5 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -97,7 +97,7 @@ <string name="pref_xmpp_resource">XMPP zdroj</string> <string name="pref_xmpp_resource_summary">Meno, ktorým sa tento klient identifikuje</string> <string name="pref_accept_files">Prijať súbory</string> - <string name="pref_accept_files_size_summary">Automaticky prijať súbory menšie ako…</string> + <string name="pref_accept_files_summary">Automaticky prijať súbory menšie ako…</string> <string name="pref_notification_settings">Nastavenia upozornení</string> <string name="pref_notifications">Upozornenia</string> <string name="pref_notifications_summary">Upozorniť pri prijatí novej správy</string> @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">Vibrovať pri prijatí novej správy</string> <string name="pref_sound">Zvuk</string> <string name="pref_sound_summary">Prehrať zvuk spolu s upozornením</string> - <string name="pref_conference_notifications">Upozornenia pri skupinovej konverzácii</string> - <string name="pref_conference_notifications_summary">Vždy upozorniť pri novej konferenčnej správe, nie len ak je zvýraznená</string> <string name="pref_notification_grace_period">Doba na prečítanie upozornenia</string> <string name="pref_notification_grace_period_summary">Neupozorňovať krátko po obdržaní kópie správy</string> <string name="pref_advanced_options">Rozšírené možnosti</string> @@ -426,7 +424,6 @@ <string name="received_location">Prijatá poloha</string> <string name="title_undo_swipe_out_conversation">Konverzácia zatvorená</string> <string name="title_undo_swipe_out_muc">Opustil skupinovú konverzáciu</string> - <string name="pref_certificate_options">Možnosti certifikátu</string> <string name="pref_dont_trust_system_cas_title">Nedôverovať systému CAs</string> <string name="pref_dont_trust_system_cas_summary">Všetky certifikáty musia byť ručne schválené</string> <string name="pref_remove_trusted_certificates_title">Odstrániť certifikáty</string> @@ -450,5 +447,7 @@ <string name="none">Žiadny</string> <string name="recently_used">Naposledy použitý</string> <string name="choose_quick_action">Vybrať rýchlu voľbu</string> - <string name="file_not_found_on_remote_host">Súbor sa na vzdialenom serveri nenašiel</string> + <string name="search_for_contacts_or_groups">Hľadať kontakty alebo skupiny</string> + <string name="send_private_message">Poslať súkromnú správu</string> + <string name="user_has_left_conference">%s opustil skupinovú konverzáciu!</string> </resources> diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 082cfa46..a8b2c889 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -78,6 +78,7 @@ <string name="choose_presence">Избор присутности за контакта</string> <string name="send_plain_text_message">Пошаљи обичну текстуалну поруку</string> <string name="send_otr_message">Пошаљи ОТР шифровану поруку</string> + <string name="send_omemo_message">Пошаљи ОМЕМО шифровану поруку</string> <string name="send_pgp_message">Пошаљи ОпенПГП шифровану поруку</string> <string name="your_nick_has_been_changed">Ваш надимак је промењен</string> <string name="send_unencrypted">Пошаљи нешифровано</string> @@ -105,8 +106,8 @@ <string name="pref_vibrate_summary">Вибрирај кад стигне нова порука</string> <string name="pref_sound">Звук</string> <string name="pref_sound_summary">Звуци обавештења</string> - <string name="pref_conference_notifications">Обавештења конференције</string> - <string name="pref_conference_notifications_summary">Увек обавести кад нова порука у конференцији стигне уместо само кад је означена</string> + <string name="pref_conference_notifications">Обавештења у јавним конференцијама</string> + <string name="pref_conference_notifications_summary">Увек обавести кад стигне порука у конференцији уместо само кад је означена</string> <string name="pref_notification_grace_period">Период одгоде обавештења</string> <string name="pref_notification_grace_period_summary">Онемогући обавештења на кратко по примању карбон копије</string> <string name="pref_advanced_options">Напредне поставке</string> @@ -152,6 +153,7 @@ <string name="encryption_choice_none">Обичан текст</string> <string name="encryption_choice_otr">ОТР</string> <string name="encryption_choice_pgp">ОпенПГП</string> + <string name="encryption_choice_omemo">ОМЕМО</string> <string name="mgmt_account_edit">Уреди налог</string> <string name="mgmt_account_delete">Обриши налог</string> <string name="mgmt_account_disable">Привремено онемогући</string> @@ -204,6 +206,13 @@ <string name="reception_failed">Примање није успело</string> <string name="your_fingerprint">Ваш отисак</string> <string name="otr_fingerprint">ОТР отисак</string> + <string name="omemo_fingerprint">ОМЕМО отисак</string> + <string name="omemo_fingerprint_selected_message">ОМЕМО отисак поруке</string> + <string name="this_device_omemo_fingerprint">Сопствени ОМЕМО отисак</string> + <string name="other_devices">Остали уређаји</string> + <string name="trust_omemo_fingerprints">Поуздај се у ОМЕМО отиске</string> + <string name="fetching_keys">Добављам кључеве...</string> + <string name="done">Готово</string> <string name="verify">Овери</string> <string name="decrypt">Дешифруј</string> <string name="conferences">Конференције</string> @@ -284,6 +293,7 @@ <string name="pref_conference_name">Име конференције</string> <string name="pref_conference_name_summary">Предмет собе уместо ЈИД-а идентификује конференцију</string> <string name="toast_message_otr_fingerprint">ОТР отисак копиран на клипборд!</string> + <string name="toast_message_omemo_fingerprint">ОМЕМО отисак копиран на клипборд!</string> <string name="conference_banned">Забрањени сте на овој конференцији</string> <string name="conference_members_only">Ова конференција је само за чланове</string> <string name="conference_kicked">Шутнути сте из ове конференције</string> @@ -350,6 +360,14 @@ <string name="reset">Ресетуј</string> <string name="account_image_description">Аватар налога</string> <string name="copy_otr_clipboard_description">Копирај ОТР отисак на клипборд</string> + <string name="copy_omemo_clipboard_description">Копирај ОМЕМО отисак на клипборд</string> + <string name="regenerate_omemo_key">Поново генериши ОМЕМО кључ</string> + <string name="wipe_omemo_pep">Уклони остале уређаје са ПЕП-а</string> + <string name="clear_other_devices">Очисти уређаје</string> + <string name="clear_other_devices_desc">Желите ли заиста да уклоните све остале уређаје са ОМЕМО објаве? Када се ваши уређаји следећи пут повежу, објавиће се сами, али у међувремену можда неће примати поруке.</string> + <string name="purge_key">Очисти кључ</string> + <string name="purge_key_desc_part1">Желите ли заиста да очистите овај кључ?</string> + <string name="purge_key_desc_part2">Неповратно ће бити сматран компровитованим, и њиме више никад нећете моћи да успоставите сесију.</string> <string name="fetching_history_from_server">Добављам историјат са сервера</string> <string name="no_more_history_on_server">Нема више историјата на серверу</string> <string name="updating">Ажурирам…</string> @@ -427,8 +445,7 @@ <string name="received_location">Примљена локација</string> <string name="title_undo_swipe_out_conversation">Преписка затворена</string> <string name="title_undo_swipe_out_muc">Напусти конференцију</string> - <string name="pref_certificate_options">Опције сертификата</string> - <string name="pref_dont_trust_system_cas_title">Не веруј системским сертификационим телима</string> + <string name="pref_dont_trust_system_cas_title">Не поуздај се у системска сертификациона тела</string> <string name="pref_dont_trust_system_cas_summary">Сви сертификати морају ручно да се одобре</string> <string name="pref_remove_trusted_certificates_title">Уклони сертификате</string> <string name="pref_remove_trusted_certificates_summary">Обриши ручно одобрене сертификате</string> @@ -451,6 +468,13 @@ <string name="none">Ниједна</string> <string name="recently_used">Недавно коришћена</string> <string name="choose_quick_action">Изаберите брзу радњу</string> - <string name="file_not_found_on_remote_host">Фајл није нађен на удаљеном серверу</string> <string name="search_for_contacts_or_groups">Тражите контакте или групе</string> + <string name="send_private_message">Пошаљи личну поруку</string> + <string name="user_has_left_conference">%s напусти конференцију!</string> + <string name="username">Корисничко име</string> + <string name="username_hint">Корисничко име</string> + <string name="invalid_username">Ово није исправно корисничко име</string> + <string name="download_failed_server_not_found">Преузимање није успело: сервер није нађен</string> + <string name="download_failed_file_not_found">Преузимање није успело: фајл није нађен</string> + <string name="download_failed_could_not_connect">Преузимање није успело: не могу да се повежем са домаћином</string> </resources> diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index d635831c..c4062588 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -78,6 +78,7 @@ <string name="choose_presence">Välj tillgänglighet till kontakt</string> <string name="send_plain_text_message">Skicka meddelande i klartext</string> <string name="send_otr_message">Skicka OTR-krypterat meddelande</string> + <string name="send_omemo_message">Skicka OMEMO-krypterat meddelande</string> <string name="send_pgp_message">Skicka OpenPGP-krypterat meddelande</string> <string name="your_nick_has_been_changed">Ditt nick har ändrats</string> <string name="send_unencrypted">Skicka okrypterat</string> @@ -97,7 +98,7 @@ <string name="pref_xmpp_resource">XMPP resurs</string> <string name="pref_xmpp_resource_summary">Namnet som klienten identifierar sig med</string> <string name="pref_accept_files">Acceptera filer</string> - <string name="pref_accept_files_size_summary">Acceptera automatiskt filer som är mindre än…</string> + <string name="pref_accept_files_summary">Acceptera automatiskt filer som är mindre än…</string> <string name="pref_notification_settings">Notifieringsinställningar</string> <string name="pref_notifications">Notifieringar</string> <string name="pref_notifications_summary">Notifiera när meddelande tagits emot</string> @@ -105,8 +106,8 @@ <string name="pref_vibrate_summary">Vibrera när meddelande tagits emot</string> <string name="pref_sound">Ljud</string> <string name="pref_sound_summary">Spela ljud med notifiering</string> - <string name="pref_conference_notifications">Konferensnotifieringar</string> - <string name="pref_conference_notifications_summary">Notifiera alltid när nytt konferensmeddelande tagits emot i stället för endast vid highlight</string> + <string name="pref_conference_notifications">Notifieringar i publika konferenser</string> + <string name="pref_conference_notifications_summary">Notifiera alltid för meddelande i en publik konferens istället för endast vid highlight</string> <string name="pref_notification_grace_period">Notifieringsfrist</string> <string name="pref_notification_grace_period_summary">Inaktivera notifieringar en kort stund efter att en carbon copy tagits emot</string> <string name="pref_advanced_options">Avancerade inställningar</string> @@ -152,6 +153,7 @@ <string name="encryption_choice_none">Klartext</string> <string name="encryption_choice_otr">OTR</string> <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> <string name="mgmt_account_edit">Ändra konto</string> <string name="mgmt_account_delete">Ta bort</string> <string name="mgmt_account_disable">Inaktivera tillfälligt</string> @@ -204,6 +206,13 @@ <string name="reception_failed">Mottagning misslyckades</string> <string name="your_fingerprint">Ditt fingeravtryck</string> <string name="otr_fingerprint">OTR-fingeravtryck</string> + <string name="omemo_fingerprint">OMEMO-fingeravtryck</string> + <string name="omemo_fingerprint_selected_message">Meddelandets OMEMO-fingeravtryck</string> + <string name="this_device_omemo_fingerprint">Eget OMEMO-fingeravtryck</string> + <string name="other_devices">Andra enheter</string> + <string name="trust_omemo_fingerprints">Lita på OMEMO-fingeravtryck</string> + <string name="fetching_keys">Hämtar nycklar...</string> + <string name="done">Klar</string> <string name="verify">Verifiera</string> <string name="decrypt">Avkryptera</string> <string name="conferences">Konferenser</string> @@ -284,6 +293,7 @@ <string name="pref_conference_name">Konferensnamn</string> <string name="pref_conference_name_summary">Använd konferensens ämne istället för JID för att identifiera konferenser</string> <string name="toast_message_otr_fingerprint">OTR-fingeravtryck har kopierats till urklipp!</string> + <string name="toast_message_omemo_fingerprint">OMEMO-fingeravtryck har kopierats till urklipp!</string> <string name="conference_banned">Du är bannlyst från denna konferens</string> <string name="conference_members_only">Medlemsskap krävs för denna konferens</string> <string name="conference_kicked">Du har blivit utsparkad från denna konferens</string> @@ -350,6 +360,14 @@ <string name="reset">Återställ</string> <string name="account_image_description">Kontots avatarbild</string> <string name="copy_otr_clipboard_description">Kopiera OTR-fingeravtryck till urklipp</string> + <string name="copy_omemo_clipboard_description">Kopiera OMEMO-fingeravtryck till urklipp</string> + <string name="regenerate_omemo_key">Regenerera OMEMO-nyckel</string> + <string name="wipe_omemo_pep">Rensa andra enheter från PEP</string> + <string name="clear_other_devices">Rensa enheter</string> + <string name="clear_other_devices_desc">Är du säker på att du vill rensa alla andra enheter från OMEMO-annonsering? Nästa gång dina enheter ansluter kommer de att återannonsera sig, men de kanske inte tar emot enheter under tiden.</string> + <string name="purge_key">Rensa nyckel</string> + <string name="purge_key_desc_part1">Är du säker på att du vill rensa denna nyckel?</string> + <string name="purge_key_desc_part2">Den kommer att antas oåterkalleligt komprometterat och du kommer aldrig kunna bygga en session med den igen.</string> <string name="fetching_history_from_server">Hämtar historik från server</string> <string name="no_more_history_on_server">Ingen mer historik på server</string> <string name="updating">Uppdaterar…</string> @@ -427,7 +445,6 @@ <string name="received_location">Mottog position</string> <string name="title_undo_swipe_out_conversation">Konversation stängd</string> <string name="title_undo_swipe_out_muc">Lämnade konferens</string> - <string name="pref_certificate_options">Certifikatalternativ</string> <string name="pref_dont_trust_system_cas_title">Lita inte på systemets CAs</string> <string name="pref_dont_trust_system_cas_summary">Alla certifikat måste manuellt godkännas</string> <string name="pref_remove_trusted_certificates_title">Ta bort certifikat</string> @@ -449,6 +466,13 @@ <string name="none">Ingen</string> <string name="recently_used">Senast använd</string> <string name="choose_quick_action">Välj snabbfunktion</string> - <string name="file_not_found_on_remote_host">Filen hittas ej på servern</string> <string name="search_for_contacts_or_groups">Sök efter kontakter eller grupper</string> + <string name="send_private_message">Skicka privat meddelande</string> + <string name="user_has_left_conference">%s har lämnat konferensen!</string> + <string name="username">Användarnamn</string> + <string name="username_hint">Användarnamn</string> + <string name="invalid_username">Inte ett giltigt användanamn</string> + <string name="download_failed_server_not_found">Nerladdning gick fel: Server hittades inte</string> + <string name="download_failed_file_not_found">Nerladdning gick fel: Filen hittades inte</string> + <string name="download_failed_could_not_connect">Nerladdningen gick fel: Kunder inte ansluta till server</string> </resources> diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml index d1679f92..8aa79ac3 100644 --- a/src/main/res/values-v21/themes.xml +++ b/src/main/res/values-v21/themes.xml @@ -6,6 +6,9 @@ <item name="android:colorPrimaryDark">@color/green700</item> <item name="android:colorAccent">@color/accent</item> + <item name="android:windowActionModeOverlay">true</item> + <item name="android:actionModeBackground">@color/accent</item> + <item name="TextSizeInfo">12sp</item> <item name="TextSizeBody">14sp</item> <item name="TextSizeHeadline">20sp</item> @@ -18,8 +21,10 @@ <item name="attr/icon_download">@drawable/ic_file_download_white_24dp</item> <item name="attr/icon_edit">@drawable/ic_edit_white_24dp</item> <item name="attr/icon_edit_dark">@drawable/ic_edit_grey600_24dp</item> + <item name="attr/icon_done">@drawable/ic_done_black_24dp</item> <item name="attr/icon_group">@drawable/ic_group_white_24dp</item> <item name="attr/icon_new">@drawable/ic_add_white_24dp</item> + <item name="attr/icon_refresh">@drawable/ic_refresh_grey600_24dp</item> <item name="attr/icon_new_attachment">@drawable/ic_attach_file_white_24dp</item> <item name="attr/icon_not_secure">@drawable/ic_lock_open_white_24dp</item> <item name="attr/icon_remove">@drawable/ic_delete_grey600_24dp</item> diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 32bfa38a..7a67ffdd 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -97,7 +97,7 @@ <string name="pref_xmpp_resource">XMPP 资源</string> <string name="pref_xmpp_resource_summary">客户端标识名称</string> <string name="pref_accept_files">接收文件</string> - <string name="pref_accept_files_size_summary">自动接收小于 … 的文件</string> + <string name="pref_accept_files_summary">自动接收小于 … 的文件</string> <string name="pref_notification_settings">通知设置</string> <string name="pref_notifications">通知</string> <string name="pref_notifications_summary">收到新消息时通知</string> @@ -105,8 +105,6 @@ <string name="pref_vibrate_summary">收到新消息时震动</string> <string name="pref_sound">声音</string> <string name="pref_sound_summary">收到新消息时的铃声</string> - <string name="pref_conference_notifications">讨论组通知</string> - <string name="pref_conference_notifications_summary">当有新的消息时总是通知而不是亮屏时才通知</string> <string name="pref_notification_grace_period">通知限期</string> <string name="pref_notification_grace_period_summary">接收副本短时间内关闭通知</string> <string name="pref_advanced_options">高级选项</string> @@ -424,7 +422,6 @@ <string name="received_location">位置已收到</string> <string name="title_undo_swipe_out_conversation">Conversation 已关闭</string> <string name="title_undo_swipe_out_muc">离开讨论组</string> - <string name="pref_certificate_options">证书选项</string> <string name="pref_dont_trust_system_cas_title">不相信系统 CA</string> <string name="pref_dont_trust_system_cas_summary">所有证书必须人工通过</string> <string name="pref_remove_trusted_certificates_title">移除证书</string> diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 95d9e4e6..a0f21acc 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -89,8 +89,6 @@ <string name="pref_vibrate_summary">收到新訊息時震動</string> <string name="pref_sound">聲音</string> <string name="pref_sound_summary">收到新訊息時播放鈴聲</string> - <string name="pref_conference_notifications">群組通知</string> - <string name="pref_conference_notifications_summary">當有新訊息時總是通知,而不是被標記時才通知</string> <string name="pref_notification_grace_period">通知限期</string> <string name="pref_notification_grace_period_summary">收到副本後,關閉通知一小段時間</string> <string name="pref_advanced_options">進階選項</string> diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 7a195aa4..0d6d19b1 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -14,6 +14,7 @@ <attr name="icon_download" format="reference"/> <attr name="icon_edit" format="reference"/> <attr name="icon_edit_dark" format="reference"/> + <attr name="icon_done" format="reference"/> <attr name="icon_group" format="reference"/> <attr name="icon_new" format="reference"/> <attr name="icon_new_attachment" format="reference"/> diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 3a778a21..c2a3ad60 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -5,12 +5,15 @@ <color name="accent">#ff0091ea</color> <color name="black87">#de000000</color> <color name="black54">#8a000000</color> + <color name="black26">#42000000</color> <color name="black12">#1f000000</color> <color name="white">#ffffffff</color> <color name="white70">#b2ffffff</color> <color name="grey50">#fffafafa</color> <color name="grey200">#ffeeeeee</color> + <color name="grey500">#ff9e9e9e</color> <color name="grey800">#ff424242</color> <color name="red500">#fff44336</color> + <color name="red800">#ffc62828</color> <color name="orange500">#ffff9800</color> </resources>
\ No newline at end of file diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index d5d4aad4..3ea590fa 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -5,7 +5,7 @@ <dimen name="infocard_padding">16dp</dimen> <dimen name="conversations_overview_width">288dp</dimen> <dimen name="image_button_padding">8dp</dimen> - <dimen name="ambilwarna_hsvHeight">240dp</dimen> + <dimen name="ambilwarna_hsvHeight">240dp</dimen> <dimen name="ambilwarna_hsvWidth">240dp</dimen> <dimen name="ambilwarna_hueWidth">30dp</dimen> <dimen name="ambilwarna_spacer">8dp</dimen> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 94d75164..9b5ccc80 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1,105 +1,105 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="app_name" translatable="false">Conversations</string> - <string name="action_settings">Settings</string> - <string name="action_add">New conversation</string> - <string name="action_accounts">Manage accounts</string> - <string name="action_end_conversation">End this conversation</string> - <string name="action_contact_details">Contact details</string> - <string name="action_muc_details">Conference details</string> - <string name="action_secure">Secure conversation</string> - <string name="action_add_account">Add account</string> - <string name="action_edit_contact">Edit name</string> - <string name="action_add_phone_book">Add to phone book</string> - <string name="action_delete_contact">Delete from roster</string> - <string name="action_block_contact">Block contact</string> - <string name="action_unblock_contact">Unblock contact</string> - <string name="action_block_domain">Block domain</string> - <string name="action_unblock_domain">Unblock domain</string> - <string name="title_activity_manage_accounts">Manage Accounts</string> - <string name="title_activity_settings">Settings</string> - <string name="title_activity_conference_details">Conference Details</string> - <string name="title_activity_contact_details">Contact Details</string> - <string name="title_activity_sharewith">Share with Conversation</string> - <string name="title_activity_start_conversation">Start Conversation</string> - <string name="title_activity_choose_contact">Choose contact</string> - <string name="title_activity_block_list">Block list</string> - <string name="just_now">just now</string> - <string name="minute_ago">1 min ago</string> - <string name="minutes_ago">%d mins ago</string> - <string name="unread_conversations">unread Conversations</string> - <string name="sending">sending…</string> - <string name="encrypted_message">Decrypting message. Please wait…</string> - <string name="nick_in_use">Nickname is already in use</string> - <string name="admin">Admin</string> - <string name="owner">Owner</string> - <string name="moderator">Moderator</string> - <string name="participant">Participant</string> - <string name="visitor">Visitor</string> - <string name="remove_contact_text">Would you like to remove %s from your roster? The conversation associated with this contact will not be removed.</string> - <string name="block_contact_text">Would you like to block %s from sending you messages?</string> - <string name="unblock_contact_text">Would you like to unblock %s and allow them to send you messages?</string> - <string name="block_domain_text">Block all contacts from %s?</string> - <string name="unblock_domain_text">Unblock all contacts from %s?</string> - <string name="contact_blocked">Contact blocked</string> - <string name="remove_bookmark_text">Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed.</string> - <string name="register_account">Register new account on server</string> - <string name="change_password_on_server">Change password on server</string> - <string name="share_with">Share with…</string> - <string name="start_conversation">Start Conversation</string> - <string name="invite_contact">Invite Contact</string> - <string name="contacts">Contacts</string> - <string name="cancel">Cancel</string> - <string name="set">Set</string> - <string name="add">Add</string> - <string name="edit">Edit</string> - <string name="delete">Delete</string> - <string name="block">Block</string> - <string name="unblock">Unblock</string> - <string name="save">Save</string> - <string name="ok">OK</string> - <string name="crash_report_title">Conversations has crashed</string> - <string name="crash_report_message">By sending in stack traces you are helping the ongoing development of Conversations\n<b>Warning:</b> This will use your XMPP account to send the stack trace to the developer.</string> - <string name="send_now">Send now</string> - <string name="send_never">Never ask again</string> - <string name="problem_connecting_to_account">Unable to connect to account</string> - <string name="problem_connecting_to_accounts">Unable to connect to multiple accounts</string> - <string name="touch_to_fix">Touch here to manage your accounts</string> - <string name="attach_file">Attach file</string> - <string name="not_in_roster">The contact is not in your roster. Would you like to add it?</string> - <string name="add_contact">Add contact</string> - <string name="send_failed">delivery failed</string> - <string name="send_rejected">rejected</string> - <string name="preparing_image">Preparing image for transmission</string> - <string name="action_clear_history">Clear history</string> - <string name="clear_conversation_history">Clear Conversation History</string> - <string name="clear_histor_msg">Do you want to delete all messages within this Conversation?\n\n<b>Warning:</b> This will not influence messages stored on other devices or servers.</string> - <string name="delete_messages">Delete messages</string> - <string name="also_end_conversation">End this conversations afterwards</string> - <string name="choose_presence">Choose presence to contact</string> - <string name="send_plain_text_message">Send plain text message</string> - <string name="send_otr_message">Send OTR encrypted message</string> - <string name="send_pgp_message">Send OpenPGP encrypted message</string> - <string name="your_nick_has_been_changed">Your nickname has been changed</string> - <string name="download_image">Download Image</string> - <string name="send_unencrypted">Send unencrypted</string> - <string name="decryption_failed">Decryption failed. Maybe you don’t have the proper private key.</string> - <string name="openkeychain_required">OpenKeychain</string> - <string name="openkeychain_required_long">Conversations utilizes a third party app called <b>OpenKeychain</b> to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n<small>(Please restart Conversations afterwards.)</small></string> - <string name="restart">Restart</string> - <string name="install">Install</string> - <string name="offering">offering…</string> - <string name="waiting">waiting…</string> - <string name="no_pgp_key">No OpenPGP Key found</string> - <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup OpenPGP.</small></string> - <string name="no_pgp_keys">No OpenPGP Keys found</string> - <string name="contacts_have_no_pgp_keys">Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\n<small>Please ask your contacts to setup OpenPGP.</small></string> - <string name="encrypted_message_received"><i>Encrypted message received. Touch to view and decrypt.</i></string> - <string name="pref_general">General</string> - <string name="pref_xmpp_resource">XMPP resource</string> - <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string> - <string name="pref_accept_files">Accept files</string> + <string name="app_name" translatable="false">Conversations</string> + <string name="action_settings">Settings</string> + <string name="action_add">New conversation</string> + <string name="action_accounts">Manage accounts</string> + <string name="action_end_conversation">End this conversation</string> + <string name="action_contact_details">Contact details</string> + <string name="action_muc_details">Conference details</string> + <string name="action_secure">Secure conversation</string> + <string name="action_add_account">Add account</string> + <string name="action_edit_contact">Edit name</string> + <string name="action_add_phone_book">Add to phone book</string> + <string name="action_delete_contact">Delete from roster</string> + <string name="action_block_contact">Block contact</string> + <string name="action_unblock_contact">Unblock contact</string> + <string name="action_block_domain">Block domain</string> + <string name="action_unblock_domain">Unblock domain</string> + <string name="title_activity_manage_accounts">Manage Accounts</string> + <string name="title_activity_settings">Settings</string> + <string name="title_activity_conference_details">Conference Details</string> + <string name="title_activity_contact_details">Contact Details</string> + <string name="title_activity_sharewith">Share with Conversation</string> + <string name="title_activity_start_conversation">Start Conversation</string> + <string name="title_activity_choose_contact">Choose contact</string> + <string name="title_activity_block_list">Block list</string> + <string name="just_now">just now</string> + <string name="minute_ago">1 min ago</string> + <string name="minutes_ago">%d mins ago</string> + <string name="unread_conversations">unread Conversations</string> + <string name="sending">sending…</string> + <string name="encrypted_message">Decrypting message. Please wait…</string> + <string name="nick_in_use">Nickname is already in use</string> + <string name="admin">Admin</string> + <string name="owner">Owner</string> + <string name="moderator">Moderator</string> + <string name="participant">Participant</string> + <string name="visitor">Visitor</string> + <string name="remove_contact_text">Would you like to remove %s from your roster? The conversation associated with this contact will not be removed.</string> + <string name="block_contact_text">Would you like to block %s from sending you messages?</string> + <string name="unblock_contact_text">Would you like to unblock %s and allow them to send you messages?</string> + <string name="block_domain_text">Block all contacts from %s?</string> + <string name="unblock_domain_text">Unblock all contacts from %s?</string> + <string name="contact_blocked">Contact blocked</string> + <string name="remove_bookmark_text">Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed.</string> + <string name="register_account">Register new account on server</string> + <string name="change_password_on_server">Change password on server</string> + <string name="share_with">Share with…</string> + <string name="start_conversation">Start Conversation</string> + <string name="invite_contact">Invite Contact</string> + <string name="contacts">Contacts</string> + <string name="cancel">Cancel</string> + <string name="set">Set</string> + <string name="add">Add</string> + <string name="edit">Edit</string> + <string name="delete">Delete</string> + <string name="block">Block</string> + <string name="unblock">Unblock</string> + <string name="save">Save</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversations has crashed</string> + <string name="crash_report_message">By sending in stack traces you are helping the ongoing development of Conversations\n<b>Warning:</b> This will use your XMPP account to send the stack trace to the developer.</string> + <string name="send_now">Send now</string> + <string name="send_never">Never ask again</string> + <string name="problem_connecting_to_account">Unable to connect to account</string> + <string name="problem_connecting_to_accounts">Unable to connect to multiple accounts</string> + <string name="touch_to_fix">Touch here to manage your accounts</string> + <string name="attach_file">Attach file</string> + <string name="not_in_roster">The contact is not in your roster. Would you like to add it?</string> + <string name="add_contact">Add contact</string> + <string name="send_failed">delivery failed</string> + <string name="send_rejected">rejected</string> + <string name="preparing_image">Preparing image for transmission</string> + <string name="action_clear_history">Clear history</string> + <string name="clear_conversation_history">Clear Conversation History</string> + <string name="clear_histor_msg">Do you want to delete all messages within this Conversation?\n\n<b>Warning:</b> This will not influence messages stored on other devices or servers.</string> + <string name="delete_messages">Delete messages</string> + <string name="also_end_conversation">End this conversations afterwards</string> + <string name="choose_presence">Choose presence to contact</string> + <string name="send_plain_text_message">Send plain text message</string> + <string name="send_otr_message">Send OTR encrypted message</string> + <string name="send_omemo_message">Send OMEMO encrypted message</string> + <string name="send_pgp_message">Send OpenPGP encrypted message</string> + <string name="your_nick_has_been_changed">Your nickname has been changed</string> + <string name="send_unencrypted">Send unencrypted</string> + <string name="decryption_failed">Decryption failed. Maybe you don’t have the proper private key.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations utilizes a third party app called <b>OpenKeychain</b> to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n<small>(Please restart Conversations afterwards.)</small></string> + <string name="restart">Restart</string> + <string name="install">Install</string> + <string name="offering">offering…</string> + <string name="waiting">waiting…</string> + <string name="no_pgp_key">No OpenPGP Key found</string> + <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup OpenPGP.</small></string> + <string name="no_pgp_keys">No OpenPGP Keys found</string> + <string name="contacts_have_no_pgp_keys">Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\n<small>Please ask your contacts to setup OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Encrypted message received. Touch to view and decrypt.</i></string> + <string name="pref_general">General</string> + <string name="pref_xmpp_resource">XMPP resource</string> + <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string> + <string name="pref_accept_files">Accept files</string> <string name="pref_accept_files_summary">Settings for accepting and automatically downloading files</string> <string name="pref_accept_files_size">Size</string> <string name="pref_accept_files_size_summary">Automatically accept files smaller than…</string> @@ -107,184 +107,193 @@ <string name="pref_accept_files_download_summary">Download and accept files automatically only when using WLAN</string> <string name="pref_accept_files_download_link">Image links</string> <string name="pref_accept_files_download_link_summary">Automatically download image links</string> - <string name="pref_notification_settings">Notification Settings</string> - <string name="pref_notifications">Notifications</string> - <string name="pref_notifications_summary">Notify when a new message arrives</string> - <string name="pref_vibrate">Vibrate</string> - <string name="pref_vibrate_summary">Also vibrate when a new message arrives</string> - <string name="pref_sound">Sound</string> - <string name="pref_sound_summary">Play ringtone with notification</string> - <string name="pref_conference_notifications">Conference notifications</string> - <string name="pref_conference_notifications_summary">Always notify when a new conference message arrives instead of only when highlighted</string> - <string name="pref_notification_grace_period">Notification grace period</string> - <string name="pref_notification_grace_period_summary">Disable notifications for a short time after a carbon copy was received</string> - <string name="pref_advanced_options">Advanced Options</string> - <string name="pref_never_send_crash">Never send crash reports</string> - <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string> - <string name="pref_confirm_messages">Confirm Messages</string> + <string name="pref_notification_settings">Notification Settings</string> + <string name="pref_notifications">Notifications</string> + <string name="pref_notifications_summary">Notify when a new message arrives</string> + <string name="pref_vibrate">Vibrate</string> + <string name="pref_vibrate_summary">Also vibrate when a new message arrives</string> + <string name="pref_sound">Sound</string> + <string name="pref_sound_summary">Play ringtone with notification</string> + <string name="pref_conference_notifications">Notifications in Public Conferences</string> + <string name="pref_conference_notifications_summary">Always notify when a message arrives in a public conference instead of only when highlighted</string> + <string name="pref_notification_grace_period">Notification grace period</string> + <string name="pref_notification_grace_period_summary">Disable notifications for a short time after a carbon copy was received</string> + <string name="pref_advanced_options">Advanced Options</string> + <string name="pref_never_send_crash">Never send crash reports</string> + <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string> + <string name="pref_confirm_messages">Confirm Messages</string> <string name="pref_confirm_messages_summary">Let your contact know when you have received or read a message</string> <string name="pref_confirm_messages_none">No confirmation</string> <string name="pref_confirm_messages_received">Confirmation for received message</string> <string name="pref_confirm_messages_read_and_received">Confirmation for received and read message</string> - <string name="pref_ui_options">UI Options</string> + <string name="pref_ui_options">UI Options</string> <string name="pref_parse_emoticons">Parse Emoticons</string> <string name="pref_parse_emoticons_summary">Replace emoticons with smilies.</string> - <string name="openpgp_error">OpenKeychain reported an error</string> - <string name="error_decrypting_file">I/O Error decrypting file</string> - <string name="accept">Accept</string> - <string name="error">An error has occurred</string> - <string name="pref_grant_presence_updates">Grant presence updates</string> - <string name="pref_grant_presence_updates_summary">Preemptively grant and ask for presence subscription for contacts you created</string> - <string name="subscriptions">Subscriptions</string> - <string name="your_account">Your account</string> - <string name="keys">Keys</string> - <string name="send_presence_updates">Send presence updates</string> - <string name="receive_presence_updates">Receive presence updates</string> - <string name="ask_for_presence_updates">Ask for presence updates</string> - <string name="attach_choose_picture">Choose picture</string> - <string name="attach_take_picture">Take picture</string> - <string name="preemptively_grant">Preemptively grant subscription request</string> - <string name="error_not_an_image_file">The file you selected is not an image</string> - <string name="error_compressing_image">Error while converting the image file</string> - <string name="error_file_not_found">File not found</string> - <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string> - <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide us with enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small></string> - <string name="account_status_unknown">Unknown</string> - <string name="account_status_disabled">Temporarily disabled</string> - <string name="account_status_online">Online</string> - <string name="account_status_connecting">Connecting\u2026</string> - <string name="account_status_offline">Offline</string> - <string name="account_status_unauthorized">Unauthorized</string> - <string name="account_status_not_found">Server not found</string> - <string name="account_status_no_internet">No connectivity</string> - <string name="account_status_regis_fail">Registration failed</string> - <string name="account_status_regis_conflict">Username already in use</string> - <string name="account_status_regis_success">Registration completed</string> - <string name="account_status_regis_not_sup">Server does not support registration</string> - <string name="account_status_security_error">Security error</string> - <string name="account_status_incompatible_server">Incompatible server</string> - <string name="encryption_choice_none">Plain text</string> - <string name="encryption_choice_otr">OTR</string> - <string name="encryption_choice_pgp">OpenPGP</string> - <string name="mgmt_account_edit">Edit account</string> - <string name="mgmt_account_delete">Delete account</string> - <string name="mgmt_account_disable">Temporarily disable</string> - <string name="mgmt_account_publish_avatar">Publish avatar</string> - <string name="mgmt_account_publish_pgp">Publish OpenPGP public key</string> - <string name="mgmt_account_enable">Enable account</string> - <string name="mgmt_account_are_you_sure">Are you sure?</string> - <string name="mgmt_account_delete_confirm_text">If you delete your account your entire conversation history will be lost</string> - <string name="attach_record_voice">Record voice</string> - <string name="account_settings_jabber_id">Jabber ID</string> - <string name="account_settings_password">Password</string> - <string name="account_settings_example_jabber_id">username@example.com</string> - <string name="account_settings_confirm_password">Confirm password</string> - <string name="password">Password</string> - <string name="confirm_password">Confirm password</string> - <string name="passwords_do_not_match">Passwords do not match</string> - <string name="invalid_jid">This is not a valid Jabber ID</string> - <string name="error_out_of_memory">Out of memory. Image is too large</string> - <string name="add_phone_book_text">Do you want to add %s to your phones contact list?</string> - <string name="contact_status_online">online</string> - <string name="contact_status_free_to_chat">free to chat</string> - <string name="contact_status_away">away</string> - <string name="contact_status_extended_away">extended away</string> - <string name="contact_status_do_not_disturb">do not disturb</string> - <string name="contact_status_offline">offline</string> - <string name="muc_details_conference">Conference</string> - <string name="muc_details_other_members">Other Members</string> - <string name="server_info_show_more">Server info</string> - <string name="server_info_mam">XEP-0313: MAM</string> - <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> - <string name="server_info_csi">XEP-0352: Client State Indication</string> - <string name="server_info_blocking">XEP-0191: Blocking Command</string> - <string name="server_info_roster_version">XEP-0237: Roster Versioning</string> - <string name="server_info_stream_management">XEP-0198: Stream Management</string> - <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> - <string name="server_info_available">available</string> - <string name="server_info_unavailable">unavailable</string> - <string name="missing_public_keys">Missing public key announcements</string> - <string name="last_seen_now">last seen just now</string> - <string name="last_seen_min">last seen 1 minute ago</string> - <string name="last_seen_mins">last seen %d minutes ago</string> - <string name="last_seen_hour">last seen 1 hour ago</string> - <string name="last_seen_hours">last seen %d hours ago</string> - <string name="last_seen_day">last seen 1 day ago</string> - <string name="last_seen_days">last seen %d days ago</string> - <string name="never_seen">never seen</string> - <string name="install_openkeychain">Encrypted message. Please install OpenKeychain to decrypt.</string> - <string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string> - <string name="openpgp_messages_found">OpenPGP encrypted messages found</string> - <string name="reception_failed">Reception failed</string> - <string name="your_fingerprint">Your fingerprint</string> - <string name="otr_fingerprint">OTR fingerprint</string> - <string name="verify">Verify</string> - <string name="decrypt">Decrypt</string> - <string name="conferences">Conferences</string> - <string name="search">Search</string> - <string name="create_contact">Create Contact</string> - <string name="join_conference">Join Conference</string> - <string name="delete_contact">Delete Contact</string> - <string name="view_contact_details">View contact details</string> - <string name="block_contact">Block contact</string> - <string name="unblock_contact">Unblock contact</string> - <string name="create">Create</string> - <string name="contact_already_exists">The contact already exists</string> - <string name="join">Join</string> - <string name="conference_address">Conference address</string> - <string name="conference_address_example">room@conference.example.com</string> - <string name="save_as_bookmark">Save as bookmark</string> - <string name="delete_bookmark">Delete bookmark</string> - <string name="bookmark_already_exists">This bookmark already exists</string> - <string name="you">You</string> - <string name="action_edit_subject">Edit conference subject</string> - <string name="conference_not_found">Conference not found</string> - <string name="leave">Leave</string> - <string name="contact_added_you">Contact added you to contact list</string> - <string name="add_back">Add back</string> - <string name="contact_has_read_up_to_this_point">%s has read up to this point</string> - <string name="publish">Publish</string> - <string name="touch_to_choose_picture">Touch avatar to select picture from gallery</string> - <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string> - <string name="publishing">Publishing…</string> - <string name="error_publish_avatar_server_reject">The server rejected your publication</string> - <string name="error_publish_avatar_converting">Something went wrong while converting your picture</string> - <string name="error_saving_avatar">Could not save avatar to disk</string> - <string name="or_long_press_for_default">(Or long press to bring back default)</string> - <string name="error_publish_avatar_no_server_support">Your server does not support the publication of avatars</string> - <string name="private_message">whispered</string> - <string name="private_message_to">to %s</string> - <string name="send_private_message_to">Send private message to %s</string> - <string name="connect">Connect</string> - <string name="account_already_exists">This account already exists</string> - <string name="next">Next</string> - <string name="server_info_session_established">Current session established</string> - <string name="additional_information">Additional Information</string> - <string name="skip">Skip</string> - <string name="disable_notifications">Disable notifications</string> - <string name="disable_notifications_for_this_conversation">Disable notifications for this conversation</string> - <string name="notifications_disabled">Notifications are disabled</string> - <string name="enable">Enable</string> - <string name="conference_requires_password">Conference requires password</string> - <string name="enter_password">Enter password</string> - <string name="missing_presence_updates">Missing presence updates from contact</string> - <string name="request_presence_updates">Please request presence updates from your contact first.\n\n<small>This will be used to determine what client(s) your contact is using.</small></string> - <string name="request_now">Request now</string> - <string name="delete_fingerprint">Delete Fingerprint</string> - <string name="sure_delete_fingerprint">Are you sure you would like to delete this fingerprint?</string> - <string name="ignore">Ignore</string> - <string name="without_mutual_presence_updates"><b>Warning:</b> Sending this without mutual presence updates could cause unexpected problems.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string> - <string name="pref_encryption_settings">Encryption settings</string> - <string name="pref_force_encryption">Force end-to-end encryption</string> - <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string> - <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string> - <string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string> - <string name="pref_expert_options">Expert options</string> - <string name="pref_expert_options_summary">Please be careful with these</string> - <string name="title_activity_about">About Conversations</string> - <string name="pref_about_conversations_summary">Build and licensing information</string> - <string name="pref_about_message" translatable="false"> - Conversations • the very last word in instant messaging. + <string name="openpgp_error">OpenKeychain reported an error</string> + <string name="error_decrypting_file">I/O Error decrypting file</string> + <string name="accept">Accept</string> + <string name="error">An error has occurred</string> + <string name="pref_grant_presence_updates">Grant presence updates</string> + <string name="pref_grant_presence_updates_summary">Preemptively grant and ask for presence subscription for contacts you created</string> + <string name="subscriptions">Subscriptions</string> + <string name="your_account">Your account</string> + <string name="keys">Keys</string> + <string name="send_presence_updates">Send presence updates</string> + <string name="receive_presence_updates">Receive presence updates</string> + <string name="ask_for_presence_updates">Ask for presence updates</string> + <string name="attach_choose_picture">Choose picture</string> + <string name="attach_take_picture">Take picture</string> + <string name="preemptively_grant">Preemptively grant subscription request</string> + <string name="error_not_an_image_file">The file you selected is not an image</string> + <string name="error_compressing_image">Error while converting the image file</string> + <string name="error_file_not_found">File not found</string> + <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string> + <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide us with enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small></string> + <string name="account_status_unknown">Unknown</string> + <string name="account_status_disabled">Temporarily disabled</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Connecting\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Unauthorized</string> + <string name="account_status_not_found">Server not found</string> + <string name="account_status_no_internet">No connectivity</string> + <string name="account_status_regis_fail">Registration failed</string> + <string name="account_status_regis_conflict">Username already in use</string> + <string name="account_status_regis_success">Registration completed</string> + <string name="account_status_regis_not_sup">Server does not support registration</string> + <string name="account_status_security_error">Security error</string> + <string name="account_status_incompatible_server">Incompatible server</string> + <string name="encryption_choice_none">Plain text</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="encryption_choice_omemo">OMEMO</string> + <string name="mgmt_account_edit">Edit account</string> + <string name="mgmt_account_delete">Delete account</string> + <string name="mgmt_account_disable">Temporarily disable</string> + <string name="mgmt_account_publish_avatar">Publish avatar</string> + <string name="mgmt_account_publish_pgp">Publish OpenPGP public key</string> + <string name="mgmt_account_enable">Enable account</string> + <string name="mgmt_account_are_you_sure">Are you sure?</string> + <string name="mgmt_account_delete_confirm_text">If you delete your account your entire conversation history will be lost</string> + <string name="attach_record_voice">Record voice</string> + <string name="account_settings_jabber_id">Jabber ID</string> + <string name="account_settings_password">Password</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">Confirm password</string> + <string name="password">Password</string> + <string name="confirm_password">Confirm password</string> + <string name="passwords_do_not_match">Passwords do not match</string> + <string name="invalid_jid">This is not a valid Jabber ID</string> + <string name="error_out_of_memory">Out of memory. Image is too large</string> + <string name="add_phone_book_text">Do you want to add %s to your phones contact list?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">free to chat</string> + <string name="contact_status_away">away</string> + <string name="contact_status_extended_away">extended away</string> + <string name="contact_status_do_not_disturb">do not disturb</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">Conference</string> + <string name="muc_details_other_members">Other Members</string> + <string name="server_info_show_more">Server info</string> + <string name="server_info_mam">XEP-0313: MAM</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_csi">XEP-0352: Client State Indication</string> + <string name="server_info_blocking">XEP-0191: Blocking Command</string> + <string name="server_info_roster_version">XEP-0237: Roster Versioning</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_http_upload">XEP-xxxx: HTTP File Upload</string> + <string name="server_info_available">available</string> + <string name="server_info_unavailable">unavailable</string> + <string name="missing_public_keys">Missing public key announcements</string> + <string name="last_seen_now">last seen just now</string> + <string name="last_seen_min">last seen 1 minute ago</string> + <string name="last_seen_mins">last seen %d minutes ago</string> + <string name="last_seen_hour">last seen 1 hour ago</string> + <string name="last_seen_hours">last seen %d hours ago</string> + <string name="last_seen_day">last seen 1 day ago</string> + <string name="last_seen_days">last seen %d days ago</string> + <string name="never_seen">never seen</string> + <string name="install_openkeychain">Encrypted message. Please install OpenKeychain to decrypt.</string> + <string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string> + <string name="openpgp_messages_found">OpenPGP encrypted messages found</string> + <string name="reception_failed">Reception failed</string> + <string name="your_fingerprint">Your fingerprint</string> + <string name="otr_fingerprint">OTR fingerprint</string> + <string name="omemo_fingerprint">OMEMO fingerprint</string> + <string name="omemo_fingerprint_selected_message">OMEMO fingerprint of message</string> + <string name="this_device_omemo_fingerprint">Own OMEMO fingerprint</string> + <string name="other_devices">Other devices</string> + <string name="trust_omemo_fingerprints">Trust OMEMO Fingerprints</string> + <string name="fetching_keys">Fetching keys...</string> + <string name="done">Done</string> + <string name="verify">Verify</string> + <string name="decrypt">Decrypt</string> + <string name="conferences">Conferences</string> + <string name="search">Search</string> + <string name="create_contact">Create Contact</string> + <string name="join_conference">Join Conference</string> + <string name="delete_contact">Delete Contact</string> + <string name="view_contact_details">View contact details</string> + <string name="block_contact">Block contact</string> + <string name="unblock_contact">Unblock contact</string> + <string name="create">Create</string> + <string name="contact_already_exists">The contact already exists</string> + <string name="join">Join</string> + <string name="conference_address">Conference address</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">Save as bookmark</string> + <string name="delete_bookmark">Delete bookmark</string> + <string name="bookmark_already_exists">This bookmark already exists</string> + <string name="you">You</string> + <string name="action_edit_subject">Edit conference subject</string> + <string name="conference_not_found">Conference not found</string> + <string name="leave">Leave</string> + <string name="contact_added_you">Contact added you to contact list</string> + <string name="add_back">Add back</string> + <string name="contact_has_read_up_to_this_point">%s has read up to this point</string> + <string name="publish">Publish</string> + <string name="touch_to_choose_picture">Touch avatar to select picture from gallery</string> + <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string> + <string name="publishing">Publishing…</string> + <string name="error_publish_avatar_server_reject">The server rejected your publication</string> + <string name="error_publish_avatar_converting">Something went wrong while converting your picture</string> + <string name="error_saving_avatar">Could not save avatar to disk</string> + <string name="or_long_press_for_default">(Or long press to bring back default)</string> + <string name="error_publish_avatar_no_server_support">Your server does not support the publication of avatars</string> + <string name="private_message">whispered</string> + <string name="private_message_to">to %s</string> + <string name="send_private_message_to">Send private message to %s</string> + <string name="connect">Connect</string> + <string name="account_already_exists">This account already exists</string> + <string name="next">Next</string> + <string name="server_info_session_established">Current session established</string> + <string name="additional_information">Additional Information</string> + <string name="skip">Skip</string> + <string name="disable_notifications">Disable notifications</string> + <string name="disable_notifications_for_this_conversation">Disable notifications for this conversation</string> + <string name="notifications_disabled">Notifications are disabled</string> + <string name="enable">Enable</string> + <string name="conference_requires_password">Conference requires password</string> + <string name="enter_password">Enter password</string> + <string name="missing_presence_updates">Missing presence updates from contact</string> + <string name="request_presence_updates">Please request presence updates from your contact first.\n\n<small>This will be used to determine what client(s) your contact is using.</small></string> + <string name="request_now">Request now</string> + <string name="delete_fingerprint">Delete Fingerprint</string> + <string name="sure_delete_fingerprint">Are you sure you would like to delete this fingerprint?</string> + <string name="ignore">Ignore</string> + <string name="without_mutual_presence_updates"><b>Warning:</b> Sending this without mutual presence updates could cause unexpected problems.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string> + <string name="pref_encryption_settings">Encryption settings</string> + <string name="pref_force_encryption">Force end-to-end encryption</string> + <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string> + <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string> + <string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string> + <string name="pref_expert_options">Expert options</string> + <string name="pref_expert_options_summary">Please be careful with these</string> + <string name="title_activity_about">About Conversations</string> + <string name="pref_about_conversations_summary">Build and licensing information</string> + <string name="pref_about_message" translatable="false"> + Conversations • the very last word in instant messaging. \n\nCopyright © 2014-2015 Daniel Gultsch \n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -308,6 +317,10 @@ \n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0) \n\nhttps://github.com/google/material-design-icons\n(CC BY 4.0) \n\nhttps://github.com/timroes/EnhancedListView\n(Apache License, Version 2.0) + \n\nhttps://github.com/leolin310148/ShortcutBadger\n(Apache License, Version 2.0) + \n\nhttps://github.com/kyleduo/SwitchButton\n(Apache License, Version 2.0) + \n\nhttps://github.com/WhisperSystems/libaxolotl-java\n(GPLv3) + \n\nhttps://github.com/vinc3m1/RoundedImageView\n(Apache License, Version 2.0) </string> <string name="title_pref_quiet_hours">Quiet Hours</string> <string name="title_pref_quiet_hours_start_time">Start time</string> @@ -324,12 +337,12 @@ <string name="pref_conference_name">Conference name</string> <string name="pref_conference_name_summary">Use room’s subject instead of JID to identify conferences</string> <string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string> + <string name="toast_message_omemo_fingerprint">OMEMO fingerprint copied to clipboard!</string> <string name="conference_banned">You are banned from this conference</string> <string name="conference_members_only">This conference is members only</string> <string name="conference_kicked">You have been kicked from this conference</string> <string name="using_account">using account %s</string> <string name="checking_x">Checking %s on HTTP host</string> - <string name="image_file_deleted">The image file has been deleted</string> <string name="not_connected_try_again">You are not connected. Try again later</string> <string name="check_x_filesize">Check %s size</string> <string name="message_options">Message options</string> @@ -391,6 +404,16 @@ <string name="reset">Reset</string> <string name="account_image_description">Account avatar</string> <string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string> + <string name="copy_omemo_clipboard_description">Copy OMEMO fingerprint to clipboard</string> + <string name="regenerate_omemo_key">Regenerate OMEMO key</string> + <string name="wipe_omemo_pep">Wipe other devices from PEP</string> + <string name="clear_other_devices">Clear devices</string> + <string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the OMEMO announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string> + <string name="purge_key">Purge key</string> + <string name="purge_key_desc_part1">Are you sure you want to purge this key?</string> + <string name="purge_key_desc_part2">It will irreversibly be considered compromised, and you can never build a session with it again.</string> + <string name="error_no_keys_to_trust">There are no usable keys available for this contact. If you have purged any of their keys, they need to generate new ones.</string> + <string name="error_trustkeys_title">Error</string> <string name="fetching_history_from_server">Fetching history from server</string> <string name="no_more_history_on_server">No more history on server</string> <string name="updating">Updating…</string> @@ -469,7 +492,6 @@ <string name="received_location">Received location</string> <string name="title_undo_swipe_out_conversation">Conversation closed</string> <string name="title_undo_swipe_out_muc">Left conference</string> - <string name="pref_certificate_options">Certificate options</string> <string name="pref_dont_trust_system_cas_title">Don’t trust system CAs</string> <string name="pref_dont_trust_system_cas_summary">All certificates must be manually approved</string> <string name="pref_remove_trusted_certificates_title">Remove certificates</string> @@ -491,8 +513,15 @@ <string name="none">None</string> <string name="recently_used">Most recently used</string> <string name="choose_quick_action">Choose quick action</string> - <string name="file_not_found_on_remote_host">File not found on remote server</string> <string name="search_for_contacts_or_groups">Search for contacts or groups</string> + <string name="send_private_message">Send private message</string> + <string name="user_has_left_conference">%s has left the conference!</string> + <string name="username">Username</string> + <string name="username_hint">Username</string> + <string name="invalid_username">This is not a valid username</string> + <string name="download_failed_server_not_found">Download failed: Server not found</string> + <string name="download_failed_file_not_found">Download failed: File not found</string> + <string name="download_failed_could_not_connect">Download failed: Could not connect to host</string> <string name="pref_led_notification_color">LED notification color</string> <string name="pref_led_notification_color_summary">Change the color of the LED notification</string> </resources> diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index b98a37fc..e8572d9d 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -4,8 +4,18 @@ <item name="android:layout_height">1.5dp</item> <item name="android:background">@color/black12</item> </style> - <style name="Tag"> - + <style name="MD"> + <item name="animationVelocity">6</item> + <item name="insetBottom">16dp</item> + <item name="insetTop">16dp</item> + <item name="insetLeft">16dp</item> + <item name="insetRight">16dp</item> + <item name="measureFactor">1.4</item> + <item name="offDrawable">@drawable/switch_back_off</item> + <item name="onDrawable">@drawable/switch_back_on</item> + <item name="thumbDrawable">@drawable/switch_thumb</item> + <item name="thumb_margin">-17dp</item> + <item name="android:padding">16dp</item> </style> </resources>
\ No newline at end of file diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 5c67203b..afdc3e80 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -18,6 +18,7 @@ <item name="attr/icon_download">@drawable/ic_action_download</item> <item name="attr/icon_edit">@drawable/ic_action_edit</item> <item name="attr/icon_edit_dark">@drawable/ic_action_edit_dark</item> + <item name="attr/icon_done">@drawable/ic_action_done</item> <item name="attr/icon_group">@drawable/ic_action_group</item> <item name="attr/icon_new">@drawable/ic_action_new</item> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 9def87c4..c858b9e6 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -1,13 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" > +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - <PreferenceCategory android:title="@string/pref_general" > + <PreferenceCategory android:title="@string/pref_general"> <CheckBoxPreference android:defaultValue="true" android:key="grant_new_contacts" android:summary="@string/pref_grant_presence_updates_summary" - android:title="@string/pref_grant_presence_updates" /> + android:title="@string/pref_grant_presence_updates"/> <ListPreference android:defaultValue="Mobile" @@ -15,7 +14,7 @@ android:entryValues="@array/resources" android:key="resource" android:summary="@string/pref_xmpp_resource_summary" - android:title="@string/pref_xmpp_resource" /> + android:title="@string/pref_xmpp_resource"/> <PreferenceScreen android:summary="@string/pref_accept_files_summary" @@ -49,13 +48,13 @@ android:entryValues="@array/confirm_values" android:key="confirm_messages_list" android:summary="@string/pref_confirm_messages_summary" - android:title="@string/pref_confirm_messages" /> + android:title="@string/pref_confirm_messages"/> <CheckBoxPreference android:defaultValue="false" android:key="chat_states" android:summary="@string/pref_chat_states_summary" - android:title="@string/pref_chat_states" /> + android:title="@string/pref_chat_states"/> <CheckBoxPreference android:defaultValue="true" android:key="parse_emoticons" @@ -66,47 +65,48 @@ <PreferenceScreen android:summary="@string/pref_notification_settings" android:title="@string/pref_notifications" > + <CheckBoxPreference + android:defaultValue="true" + android:key="show_notification" + android:summary="@string/pref_notifications_summary" + android:title="@string/pref_notifications"/> + <PreferenceScreen + android:dependency="show_notification" + android:key="quiet_hours" + android:summary="@string/pref_quiet_hours_summary" + android:title="@string/title_pref_quiet_hours"> <CheckBoxPreference - android:defaultValue="true" - android:key="show_notification" - android:summary="@string/pref_notifications_summary" - android:title="@string/pref_notifications" /> - <PreferenceScreen - android:dependency="show_notification" + android:defaultValue="false" + android:key="enable_quiet_hours" android:summary="@string/pref_quiet_hours_summary" - android:title="@string/title_pref_quiet_hours"> - <CheckBoxPreference - android:defaultValue="false" - android:key="enable_quiet_hours" - android:summary="@string/pref_quiet_hours_summary" - android:title="@string/title_pref_enable_quiet_hours" /> - <eu.siacs.conversations.ui.TimePreference - android:dependency="enable_quiet_hours" - android:key="quiet_hours_start" - android:negativeButtonText="@string/cancel" - android:positiveButtonText="@string/set" - android:title="@string/title_pref_quiet_hours_start_time" /> - <eu.siacs.conversations.ui.TimePreference - android:dependency="enable_quiet_hours" - android:key="quiet_hours_end" - android:negativeButtonText="@string/cancel" - android:positiveButtonText="@string/set" - android:title="@string/title_pref_quiet_hours_end_time" /> - </PreferenceScreen> - <CheckBoxPreference - android:defaultValue="true" - android:dependency="show_notification" - android:key="vibrate_on_notification" - android:summary="@string/pref_vibrate_summary" - android:title="@string/pref_vibrate" /> + android:title="@string/title_pref_enable_quiet_hours"/> + <eu.siacs.conversations.ui.TimePreference + android:dependency="enable_quiet_hours" + android:key="quiet_hours_start" + android:negativeButtonText="@string/cancel" + android:positiveButtonText="@string/set" + android:title="@string/title_pref_quiet_hours_start_time"/> + <eu.siacs.conversations.ui.TimePreference + android:dependency="enable_quiet_hours" + android:key="quiet_hours_end" + android:negativeButtonText="@string/cancel" + android:positiveButtonText="@string/set" + android:title="@string/title_pref_quiet_hours_end_time"/> + </PreferenceScreen> + <CheckBoxPreference + android:defaultValue="true" + android:dependency="show_notification" + android:key="vibrate_on_notification" + android:summary="@string/pref_vibrate_summary" + android:title="@string/pref_vibrate"/> - <RingtonePreference - android:defaultValue="content://settings/system/notification_sound" - android:dependency="show_notification" - android:key="notification_ringtone" - android:ringtoneType="notification" - android:summary="@string/pref_sound_summary" - android:title="@string/pref_sound" /> + <RingtonePreference + android:defaultValue="content://settings/system/notification_sound" + android:dependency="show_notification" + android:key="notification_ringtone" + android:ringtoneType="notification" + android:summary="@string/pref_sound_summary" + android:title="@string/pref_sound"/> <yuku.ambilwarna.widget.AmbilWarnaPreference android:defaultValue="0xffffffff" @@ -115,97 +115,90 @@ app:supportsAlpha="true" android:summary="@string/pref_led_notification_color_summary"/> - <CheckBoxPreference - android:defaultValue="true" - android:dependency="show_notification" - android:key="always_notify_in_conference" - android:summary="@string/pref_conference_notifications_summary" - android:title="@string/pref_conference_notifications" /> + <CheckBoxPreference + android:defaultValue="false" + android:dependency="show_notification" + android:key="always_notify_in_conference" + android:summary="@string/pref_conference_notifications_summary" + android:title="@string/pref_conference_notifications"/> </PreferenceScreen> </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_ui_options" > + <PreferenceCategory android:title="@string/pref_ui_options"> <CheckBoxPreference android:defaultValue="true" android:key="use_subject" android:summary="@string/pref_conference_name_summary" - android:title="@string/pref_conference_name" /> + android:title="@string/pref_conference_name"/> <CheckBoxPreference android:defaultValue="false" android:key="use_larger_font" android:summary="@string/pref_use_larger_font_summary" - android:title="@string/pref_use_larger_font" /> + android:title="@string/pref_use_larger_font"/> <CheckBoxPreference android:defaultValue="false" android:key="send_button_status" android:summary="@string/pref_use_send_button_to_indicate_status_summary" - android:title="@string/pref_use_send_button_to_indicate_status" /> + android:title="@string/pref_use_send_button_to_indicate_status"/> <ListPreference - android:key="quick_action" android:defaultValue="recent" + android:dialogTitle="@string/choose_quick_action" android:entries="@array/quick_actions" android:entryValues="@array/quick_action_values" + android:key="quick_action" android:summary="@string/pref_quick_action_summary" - android:title="@string/pref_quick_action" - android:dialogTitle="@string/choose_quick_action"/> + android:title="@string/pref_quick_action"/> <CheckBoxPreference android:defaultValue="false" android:key="show_dynamic_tags" android:summary="@string/pref_show_dynamic_tags_summary" - android:title="@string/pref_show_dynamic_tags" /> + android:title="@string/pref_show_dynamic_tags"/> </PreferenceCategory> <PreferenceCategory - android:title="@string/pref_advanced_options" - android:key="advanced"> + android:key="advanced" + android:title="@string/pref_advanced_options"> <PreferenceScreen + android:key="expert" android:summary="@string/pref_expert_options_summary" - android:title="@string/pref_expert_options" - android:key="expert"> - <PreferenceCategory android:title="@string/pref_encryption_settings" > - <CheckBoxPreference - android:defaultValue="false" - android:key="force_encryption" - android:summary="@string/pref_force_encryption_summary" - android:title="@string/pref_force_encryption" /> + android:title="@string/pref_expert_options"> + <PreferenceCategory android:title="@string/pref_encryption_settings"> <CheckBoxPreference android:defaultValue="false" android:key="dont_save_encrypted" android:summary="@string/pref_dont_save_encrypted_summary" - android:title="@string/pref_dont_save_encrypted" /> + android:title="@string/pref_dont_save_encrypted"/> + <CheckBoxPreference + android:defaultValue="false" + android:key="dont_trust_system_cas" + android:summary="@string/pref_dont_trust_system_cas_summary" + android:title="@string/pref_dont_trust_system_cas_title"/> + <Preference + android:key="remove_trusted_certificates" + android:summary="@string/pref_remove_trusted_certificates_summary" + android:title="@string/pref_remove_trusted_certificates_title"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_input_options"> + <CheckBoxPreference + android:defaultValue="false" + android:key="enter_is_send" + android:summary="@string/pref_enter_is_send_summary" + android:title="@string/pref_enter_is_send"/> + <CheckBoxPreference + android:defaultValue="false" + android:key="display_enter_key" + android:summary="@string/pref_display_enter_key_summary" + android:title="@string/pref_display_enter_key"/> </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_input_options"> - <CheckBoxPreference - android:defaultValue="false" - android:key="enter_is_send" - android:title="@string/pref_enter_is_send" - android:summary="@string/pref_enter_is_send_summary" /> - <CheckBoxPreference - android:defaultValue="false" - android:key="display_enter_key" - android:title="@string/pref_display_enter_key" - android:summary="@string/pref_display_enter_key_summary" /> - </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_certificate_options"> - <CheckBoxPreference - android:defaultValue="false" - android:key="dont_trust_system_cas" - android:title="@string/pref_dont_trust_system_cas_title" - android:summary="@string/pref_dont_trust_system_cas_summary" /> - <Preference - android:key="remove_trusted_certificates" - android:title="@string/pref_remove_trusted_certificates_title" - android:summary="@string/pref_remove_trusted_certificates_summary" /> - </PreferenceCategory> - <PreferenceCategory android:title="@string/pref_expert_options_other" > + <PreferenceCategory android:title="@string/pref_expert_options_other"> <CheckBoxPreference android:defaultValue="false" android:key="indicate_received" android:summary="@string/pref_use_indicate_received_summary" - android:title="@string/pref_use_indicate_received" /> + android:title="@string/pref_use_indicate_received"/> <CheckBoxPreference android:defaultValue="false" android:key="keep_foreground_service" - android:title="@string/pref_keep_foreground_service" - android:summary="@string/pref_keep_foreground_service_summary" /> + android:summary="@string/pref_keep_foreground_service_summary" + android:title="@string/pref_keep_foreground_service"/> </PreferenceCategory> </PreferenceScreen> @@ -213,9 +206,9 @@ android:defaultValue="false" android:key="never_send" android:summary="@string/pref_never_send_crash_summary" - android:title="@string/pref_never_send_crash" /> + android:title="@string/pref_never_send_crash"/> </PreferenceCategory> - <eu.siacs.conversations.ui.AboutPreference - android:summary="@string/pref_about_conversations_summary" - android:title="@string/title_activity_about" /> + <eu.siacs.conversations.ui.AboutPreference + android:summary="@string/pref_about_conversations_summary" + android:title="@string/title_activity_about"/> </PreferenceScreen> |