diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fbbaf83..64413709 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
###Changelog
+####Version 1.6.0
+* new multi-end-to-multi-end encryption method
+* redesigned chat bubbles
+* show unexpected encryption changes as red chat bubbles
+* always notify in private/non-anonymous conferences
+
####Version 1.5.1
* fixed rare crashes
* improved otr support
diff --git a/README.md b/README.md
index 9e7ffd2f..a68b162b 100644
--- a/README.md
+++ b/README.md
@@ -39,24 +39,24 @@ support these extensions; therefore to get the most out of Conversations you
should consider either switching to an XMPP server that does or — even better —
run your own XMPP server for you and your friends. These XEP's are:
-* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer
+* [XEP-0065: SOCKS5 Bytestreams](http://xmpp.org/extensions/xep-0065.html) (or mod_proxy65). Will be used to transfer
files if both parties are behind a firewall (NAT).
-* XEP-0163: Personal Eventing Protocol for avatars
-* XEP-0191: Blocking command lets you blacklist spammers or block contacts
+* [XEP-0163: Personal Eventing Protocol](http://xmpp.org/extensions/xep-0163.html) for avatars
+* [XEP-0191: Blocking command](http://xmpp.org/extensions/xep-0191.html) lets you blacklist spammers or block contacts
without removing them from your roster.
-* XEP-0198: Stream Management allows XMPP to survive small network outages and
+* [XEP-0198: Stream Management](http://xmpp.org/extensions/xep-0198.html) allows XMPP to survive small network outages and
changes of the underlying TCP connection.
-* XEP-0280: Message Carbons which automatically syncs the messages you send to
+* [XEP-0280: Message Carbons](http://xmpp.org/extensions/xep-0280.html) which automatically syncs the messages you send to
your desktop client and thus allows you to switch seamlessly from your mobile
client to your desktop client and back within one conversation.
-* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections
-* XEP-0313: Message Archive Management synchronize message history with the
+* [XEP-0237: Roster Versioning](http://xmpp.org/extensions/xep-0237.html) mainly to save bandwidth on poor mobile connections
+* [XEP-0313: Message Archive Management](http://xmpp.org/extensions/xep-0313.html) synchronize message history with the
server. Catch up with messages that were sent while Conversations was
offline.
-* XEP-0352: Client State Indication lets the server know whether or not
+* [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html) lets the server know whether or not
Conversations is in the background. Allows the server to save bandwidth by
withholding unimportant packages.
-* XEP-xxxx: HttpUpload allows you to share files in conferences and with offline
+* [XEP-xxxx: HTTP File Upload](http://xmpp.org/extensions/inbox/http-upload.html) allows you to share files in conferences and with offline
contacts. Requires an [additional component](https://github.com/siacs/HttpUploadComponent)
on your server.
@@ -81,6 +81,7 @@ run your own XMPP server for you and your friends. These XEP's are:
#### Logo
* [Ilia Rostovtsev](https://github.com/qooob) (Progress)
* [Diego Turtulici](http://efesto.eigenlab.org/~diesys) (Original)
+* [fiaxh](https://github.com/fiaxh) (OMEMO)
#### Translations
Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/)
diff --git a/art/md_switch_thumb_disable.svg b/art/md_switch_thumb_disable.svg
new file mode 100644
index 00000000..efd83c2d
--- /dev/null
+++ b/art/md_switch_thumb_disable.svg
@@ -0,0 +1,156 @@
+
+
+
+
diff --git a/art/md_switch_thumb_off_normal.svg b/art/md_switch_thumb_off_normal.svg
new file mode 100644
index 00000000..25d1761d
--- /dev/null
+++ b/art/md_switch_thumb_off_normal.svg
@@ -0,0 +1,153 @@
+
+
+
+
diff --git a/art/md_switch_thumb_off_pressed.svg b/art/md_switch_thumb_off_pressed.svg
new file mode 100644
index 00000000..002b4781
--- /dev/null
+++ b/art/md_switch_thumb_off_pressed.svg
@@ -0,0 +1,159 @@
+
+
+
+
diff --git a/art/md_switch_thumb_on_normal.svg b/art/md_switch_thumb_on_normal.svg
new file mode 100644
index 00000000..5e8f90f3
--- /dev/null
+++ b/art/md_switch_thumb_on_normal.svg
@@ -0,0 +1,146 @@
+
+
+
+
diff --git a/art/md_switch_thumb_on_pressed.svg b/art/md_switch_thumb_on_pressed.svg
new file mode 100644
index 00000000..e0331e7b
--- /dev/null
+++ b/art/md_switch_thumb_on_pressed.svg
@@ -0,0 +1,162 @@
+
+
+
+
diff --git a/art/message_bubble_received.svg b/art/message_bubble_received.svg
new file mode 100644
index 00000000..815892ed
--- /dev/null
+++ b/art/message_bubble_received.svg
@@ -0,0 +1,165 @@
+
+
+
+
diff --git a/art/message_bubble_received_warning.svg b/art/message_bubble_received_warning.svg
new file mode 100644
index 00000000..9353492b
--- /dev/null
+++ b/art/message_bubble_received_warning.svg
@@ -0,0 +1,165 @@
+
+
+
+
diff --git a/art/message_bubble_sent.svg b/art/message_bubble_sent.svg
new file mode 100644
index 00000000..90ad5091
--- /dev/null
+++ b/art/message_bubble_sent.svg
@@ -0,0 +1,165 @@
+
+
+
+
diff --git a/art/omemo_logo.svg b/art/omemo_logo.svg
new file mode 100644
index 00000000..ca20a5b9
--- /dev/null
+++ b/art/omemo_logo.svg
@@ -0,0 +1,273 @@
+
+
+
+
diff --git a/art/render.rb b/art/render.rb
index 698abea5..dc55c327 100755
--- a/art/render.rb
+++ b/art/render.rb
@@ -1,11 +1,15 @@
#!/bin/env ruby
-resolutions={
- 'mdpi'=> 1,
+
+require 'xml'
+
+resolutions = {
+ 'mdpi' => 1,
'hdpi' => 1.5,
'xhdpi' => 2,
'xxhdpi' => 3,
'xxxhdpi' => 4,
}
+
images = {
'conversations_baloon.svg' => ['ic_launcher', 48],
'conversations_mono.svg' => ['ic_notification', 24],
@@ -14,33 +18,111 @@ images = {
'ic_send_text_online.svg' => ['ic_send_text_online', 36],
'ic_send_text_away.svg' => ['ic_send_text_away', 36],
'ic_send_text_dnd.svg' => ['ic_send_text_dnd', 36],
- 'ic_send_photo_online.svg' => ['ic_send_photo_online', 36],
- 'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36],
- 'ic_send_photo_away.svg' => ['ic_send_photo_away', 36],
- 'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36],
+ 'ic_send_photo_online.svg' => ['ic_send_photo_online', 36],
+ 'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36],
+ 'ic_send_photo_away.svg' => ['ic_send_photo_away', 36],
+ 'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36],
'ic_send_location_online.svg' => ['ic_send_location_online', 36],
- 'ic_send_location_offline.svg' => ['ic_send_location_offline', 36],
- 'ic_send_location_away.svg' => ['ic_send_location_away', 36],
- 'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36],
+ 'ic_send_location_offline.svg' => ['ic_send_location_offline', 36],
+ 'ic_send_location_away.svg' => ['ic_send_location_away', 36],
+ 'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36],
'ic_send_voice_online.svg' => ['ic_send_voice_online', 36],
- 'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36],
- 'ic_send_voice_away.svg' => ['ic_send_voice_away', 36],
- 'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36],
+ 'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36],
+ 'ic_send_voice_away.svg' => ['ic_send_voice_away', 36],
+ 'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36],
'ic_send_cancel_online.svg' => ['ic_send_cancel_online', 36],
- 'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36],
- 'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36],
- 'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36],
- 'ic_send_picture_online.svg' => ['ic_send_picture_online', 36],
- 'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36],
- 'ic_send_picture_away.svg' => ['ic_send_picture_away', 36],
- 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36]
+ 'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36],
+ 'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36],
+ 'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36],
+ 'ic_send_picture_online.svg' => ['ic_send_picture_online', 36],
+ 'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36],
+ 'ic_send_picture_away.svg' => ['ic_send_picture_away', 36],
+ 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36],
+ 'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48],
+ 'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48],
+ 'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48],
+ 'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48],
+ 'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48],
+ 'message_bubble_received.svg' => ['message_bubble_received.9', 0],
+ 'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0],
+ 'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],
}
-images.each do |source, result|
- resolutions.each do |name, factor|
- size = factor * result[1]
- path = "../src/main/res/drawable-#{name}/#{result[0]}.png"
- cmd = "inkscape -e #{path} -C -h #{size} -w #{size} #{source}"
- puts cmd
- system cmd
+
+# Executable paths for Mac OSX
+# "/Applications/Inkscape.app/Contents/Resources/bin/inkscape"
+
+inkscape = "inkscape"
+imagemagick = "convert"
+
+def execute_cmd(cmd)
+ puts cmd
+ system cmd
+end
+
+images.each do |source_filename, settings|
+ svg_content = File.read(source_filename)
+
+ svg = XML::Document.string(svg_content)
+ base_width = svg.root["width"].to_i
+ base_height = svg.root["height"].to_i
+
+ guides = svg.find(".//sodipodi:guide")
+
+ resolutions.each do |resolution, factor|
+ output_filename, base_size = settings
+
+ if base_size > 0
+ width = factor * base_size
+ height = factor * base_size
+ else
+ width = factor * base_width
+ height = factor * base_height
+ end
+
+ path = "../src/main/res/drawable-#{resolution}/#{output_filename}.png"
+ execute_cmd "#{inkscape} -f #{source_filename} -z -C -w #{width} -h #{height} -e #{path}"
+
+ top = []
+ right = []
+ bottom = []
+ left = []
+
+ guides.each do |guide|
+ orientation = guide["orientation"]
+ x, y = guide["position"].split(",")
+ x, y = x.to_i, y.to_i
+
+ if orientation == "1,0" and y == base_height
+ top.push(x * factor)
+ end
+
+ if orientation == "0,1" and x == base_width
+ right.push((base_height - y) * factor)
+ end
+
+ if orientation == "1,0" and y == 0
+ bottom.push(x * factor)
+ end
+
+ if orientation == "0,1" and x == 0
+ left.push((base_height - y) * factor)
+ end
+ end
+
+ next if top.length != 2
+ next if right.length != 2
+ next if bottom.length != 2
+ next if left.length != 2
+
+ execute_cmd "#{imagemagick} -background none PNG32:#{path} -gravity center -extent #{width+2}x#{height+2} PNG32:#{path}"
+
+ draw_format = "-draw \"rectangle %d,%d %d,%d\""
+ top_line = draw_format % [top.min + 1, 0, top.max, 0]
+ right_line = draw_format % [width + 1, right.min + 1, width + 1, right.max]
+ bottom_line = draw_format % [bottom.min + 1, height + 1, bottom.max, height + 1]
+ left_line = draw_format % [0, left.min + 1, 0, left.max]
+ draws = "#{top_line} #{right_line} #{bottom_line} #{left_line}"
+
+ execute_cmd "#{imagemagick} -background none PNG32:#{path} -fill black -stroke none #{draws} PNG32:#{path}"
end
end
diff --git a/build.gradle b/build.gradle
index 639e9a7f..9e1ac078 100644
--- a/build.gradle
+++ b/build.gradle
@@ -35,7 +35,10 @@ dependencies {
compile 'com.google.zxing:android-integration:3.1.0'
compile 'de.measite.minidns:minidns:0.1.3'
compile 'de.timroes.android:EnhancedListView:0.3.4'
- compile 'me.leolin:ShortcutBadger:1.1.1@aar'
+ compile 'me.leolin:ShortcutBadger:1.1.3@aar'
+ compile 'com.kyleduo.switchbutton:library:1.2.8'
+ compile 'org.whispersystems:axolotl-android:1.3.4'
+ compile 'com.makeramen:roundedimageview:2.1.1'
}
android {
@@ -45,8 +48,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 21
- versionCode 80
- versionName "1.5.2"
+ versionCode 82
+ versionName "1.6.0-beta.2"
}
compileOptions {
@@ -93,7 +96,7 @@ android {
}
lintOptions {
- disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity'
+ disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource'
}
subprojects {
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 9fe37017..15d5bc25 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -130,6 +130,10 @@
+
> deviceIds;
+ private final Map messageCache;
+ private final FetchStatusMap fetchStatusMap;
+ private final SerialSingleThreadExecutor executor;
+
+ private static class AxolotlAddressMap {
+ protected Map> 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 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 devices = map.get(address.getName());
+ if (devices == null) {
+ return null;
+ }
+ return devices.get(address.getDeviceId());
+ }
+ }
+
+ public Map getAll(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map devices = map.get(address.getName());
+ if (devices == null) {
+ return new HashMap<>();
+ }
+ return devices;
+ }
+ }
+
+ public boolean hasAny(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map devices = map.get(address.getName());
+ return devices != null && !devices.isEmpty();
+ }
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ }
+
+ private static class SessionMap extends AxolotlAddressMap {
+ 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 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 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 {
+
+ }
+
+ 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 getKeysWithTrust(XmppAxolotlSession.Trust trust) {
+ return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
+ }
+
+ public Set 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 findOwnSessions() {
+ AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
+ Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
+ return ownDeviceSessions;
+ }
+
+ private Set findSessionsforContact(Contact contact) {
+ AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
+ Set 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 getOwnDeviceIds() {
+ return this.deviceIds.get(account.getJid().toBareJid());
+ }
+
+ private void setTrustOnSessions(final Jid jid, @NonNull final Set 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 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 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 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 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) {
+ Element item = mXmppConnectionService.getIqParser().getItem(packet);
+ Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+ if (deviceIds == null) {
+ deviceIds = new HashSet();
+ }
+ 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!
+ }
+ });
+ }
+ }
+ });
+ }
+
+ 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) {
+ PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
+ Map 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 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 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;
+ }
+ }
+ });
+ }
+
+ 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) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
+ final IqParser parser = mXmppConnectionService.getIqParser();
+ final List 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();
+ }
+ });
+ } catch (InvalidJidException e) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
+ }
+ }
+
+ public Set 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 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 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 contactSessions = findSessionsforContact(contact);
+ Set 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 trustCache =
+ new LruCache(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.
+ *
+ * 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
+ *
+ * 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.
+ *
+ * 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 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.
+ *
+ * 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 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 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 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 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 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 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/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index f472361f..7a2dc3f7 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;
@@ -254,6 +256,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);
@@ -277,8 +283,13 @@ public class Account extends AbstractEntity {
return values;
}
+ public AxolotlService getAxolotlService() {
+ return axolotlService;
+ }
+
public void initAccountServices(final XmppConnectionService 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 e546f214..f924c05a 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -183,20 +183,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() {
@@ -281,60 +283,65 @@ public class Contact implements ListItem, Blockable {
}
public ArrayList getOtrFingerprints() {
- final ArrayList fingerprints = new ArrayList();
- 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 fingerprints = new ArrayList();
+ 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) {
+ }
}
}
@@ -441,24 +448,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 289ed4ea..9f9f34cf 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;
- }
- }
-
- 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 getMostRecentlyUsedOutgoingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(0);
+ 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 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;
+ }
+
+ private int getMostRecentlyUsedIncomingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(0);
+ 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 e;
+ }
+ }
+ }
}
+ 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,37 +1,16 @@
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;
-
public class DownloadableFile extends File {
private static final long serialVersionUID = 2247012619505115863L;
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() {
+ public void setKey(byte[] key) {
+ this.aeskey = key;
+ }
+
+ public void setIv(byte[] iv) {
+ this.iv = iv;
+ }
+
+ public byte[] getKey() {
return this.aeskey;
}
- 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 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[] 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 957c2a6d..0eff99cf 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;
}
@@ -391,7 +417,8 @@ public class Message extends AbstractEntity {
!message.getBody().startsWith(ME_COMMAND) &&
!this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() &&
- !message.bodyIsHeart()
+ !message.bodyIsHeart() &&
+ this.isTrusted() == message.isTrusted()
);
}
@@ -407,11 +434,14 @@ public class Message extends AbstractEntity {
}
public String getMergedBody() {
- final Message next = this.next();
- if (this.mergeable(next)) {
- return getBody().trim() + 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().trim());
}
- return getBody().trim();
+ return body.toString();
}
public boolean hasMeCommand() {
@@ -419,20 +449,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() {
@@ -663,4 +696,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 79626511..69bb1803 100644
--- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -12,6 +12,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
@@ -28,7 +29,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 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 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 62fe4191..0d202bb9 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;
@@ -161,20 +176,23 @@ public class HttpDownloadConnection implements Transferable {
}
private long retrieveFileSize() throws IOException {
- Log.d(Config.LOGTAG,"retrieve file size. interactive:"+String.valueOf(interactive));
- changeStatus(STATUS_CHECKING);
- HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
- connection.setRequestMethod("HEAD");
- if (connection instanceof HttpsURLConnection) {
- mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
- }
- connection.connect();
- String contentLength = connection.getHeaderField("Content-Length");
- if (contentLength == null) {
- throw new IOException();
- }
try {
+ Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
+ changeStatus(STATUS_CHECKING);
+ HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
+ connection.setRequestMethod("HEAD");
+ if (connection instanceof HttpsURLConnection) {
+ mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ String contentLength = connection.getHeaderField("Content-Length");
+ connection.disconnect();
+ if (contentLength == null) {
+ throw new IOException();
+ }
return Long.parseLong(contentLength, 10);
+ } catch (IOException e) {
+ throw e;
} catch (NumberFormatException e) {
throw new IOException();
}
@@ -186,6 +204,8 @@ public class HttpDownloadConnection implements Transferable {
private boolean interactive = false;
+ private OutputStream os;
+
public FileDownloader(boolean interactive) {
this.interactive = interactive;
}
@@ -200,36 +220,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);
+ 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();
}
- 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));
- }
- os.flush();
- os.close();
- is.close();
}
private void updateImageBounds() {
@@ -242,10 +270,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 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() {
@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 24e93db1..18331796 100644
--- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -11,7 +11,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public abstract class AbstractParser {
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
index 6039d395..e74cb65c 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,6 +85,155 @@ 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 deviceIds(final Element item) {
+ Set 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 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 preKeyPublics(final IqPacket packet) {
+ Map preKeyRecords = new HashMap<>();
+ Element item = getItem(packet);
+ if (item == null) {
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find - 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 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 preKeys(final IqPacket preKeys) {
+ List bundles = new ArrayList<>();
+ Map 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)) {
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 2d722af7..025ed1e7 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -6,7 +6,11 @@ import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
+import java.util.Set;
+
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;
@@ -69,11 +73,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;
@@ -94,6 +96,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;
@@ -170,6 +186,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 deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+ AxolotlService axolotlService = account.getAxolotlService();
+ axolotlService.registerDevices(from, deviceIds);
+ mXmppConnectionService.updateAccountUi();
}
}
@@ -177,6 +200,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(),
@@ -198,6 +228,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) {
@@ -228,7 +259,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;
@@ -238,8 +270,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();
@@ -267,11 +300,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())) {
@@ -300,14 +333,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) {
@@ -338,15 +377,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);
}
}
@@ -433,4 +476,4 @@ public class MessageParser extends AbstractParser implements
contact.setPresenceName(nick);
}
}
-}
\ No newline at end of file
+}
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 getSubDeviceSessions(Account account, AxolotlAddress contact) {
+ List 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 loadSignedPreKeys(Account account) {
+ List 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 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 loadIdentityKeys(Account account, String name) {
+ return loadIdentityKeys(account, name, null);
+ }
+
+ public Set loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
+ Set 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..6e5a1ae3 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -1,22 +1,5 @@
package eu.siacs.conversations.persistance;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.security.DigestOutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.Locale;
-
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -31,14 +14,33 @@ import android.util.Base64OutputStream;
import android.util.Log;
import android.webkit.MimeTypeMap;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+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;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
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,25 @@ 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;
}
- return path;
+ Log.d(Config.LOGTAG,"using image as is. path: "+path);
+ 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;
+ }
+ }
+
+ 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 +186,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 +217,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);
}
@@ -546,4 +558,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 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(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(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/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 956f704e..90e4d216 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -64,7 +64,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)
);
@@ -332,9 +332,10 @@ public class NotificationService {
private Message getImage(final Iterable 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 f5c54adf..a9a2f211 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -52,16 +52,17 @@ import de.duenndns.ssl.MemorizingTrustManager;
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;
@@ -85,6 +86,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;
@@ -273,11 +275,13 @@ 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) {
@@ -304,6 +308,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;
@@ -342,7 +348,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void attachLocationToConversation(final Conversation conversation,
final Uri uri,
final UiCallback callback) {
- int encryption = conversation.getNextEncryption(forceEncryption());
+ int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) {
encryption = Message.ENCRYPTION_DECRYPTED;
}
@@ -361,12 +367,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final Uri uri,
final UiCallback 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);
@@ -399,15 +403,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
- public void attachImageToConversation(final Conversation conversation,
- final Uri uri, final UiCallback callback) {
+ public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback 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);
@@ -417,7 +423,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);
@@ -591,9 +597,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);
@@ -674,22 +677,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();
@@ -699,7 +702,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);
@@ -712,24 +716,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:
@@ -743,7 +747,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()) {
@@ -753,6 +757,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) {
@@ -780,6 +802,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;
}
}
@@ -799,6 +824,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()));
@@ -813,13 +841,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) {
@@ -830,7 +858,7 @@ 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());
+ iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
sendIqPacket(account,iqPacket,mIqParser);
}
@@ -943,6 +971,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();
@@ -974,6 +1003,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);
+ }
}
}
});
@@ -985,7 +1018,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;
}
@@ -1342,6 +1380,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()) {
@@ -1372,7 +1435,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() {
@@ -1784,7 +1848,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) {
@@ -1797,8 +1861,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);
@@ -2260,6 +2325,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())) {
@@ -2470,8 +2541,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 54c064c6..aa986bd1 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;
@@ -104,4 +103,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.Collections;
-import java.util.List;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+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() {
@@ -157,6 +161,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
refreshUi();
}
+ @Override
+ public void OnUpdateBlocklist(final Status status) {
+ refreshUi();
+ }
+
@Override
protected void refreshUiReal() {
invalidateOptionsMenu();
@@ -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 96abf65b..219a4fca 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;
public class ConversationActivity extends XmppActivity
implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
@@ -58,15 +64,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";
@@ -76,6 +86,7 @@ public class ConversationActivity extends XmppActivity
final private List mPendingImageUris = new ArrayList<>();
final private List mPendingFileUris = new ArrayList<>();
private Uri mPendingGeoUri = null;
+ private boolean forbidProcessingPendings = false;
private View mContentView;
@@ -374,7 +385,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 {
@@ -385,6 +396,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);
}
@@ -398,7 +410,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() {
@@ -456,7 +468,7 @@ public class ConversationActivity extends XmppActivity
conversation.setNextCounterpart(null);
callback.onPresenceSelected();
} else {
- selectPresence(conversation,callback);
+ selectPresence(conversation, callback);
}
}
@@ -466,7 +478,7 @@ public class ConversationActivity extends XmppActivity
if (intent.resolveActivity(getPackageManager()) != null) {
return intent;
} else {
- intent.setData(Uri.parse("http://play.google.com/store/apps/details?id="+packageId));
+ intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
return intent;
}
}
@@ -487,7 +499,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) {
@@ -534,7 +546,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);
+ }
}
}
@@ -749,6 +763,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;
@@ -756,6 +776,7 @@ public class ConversationActivity extends XmppActivity
xmppConnectionService.databaseBackend.updateConversation(conversation);
fragment.updateChatMsgHint();
invalidateOptionsMenu();
+ refreshUi();
return true;
}
});
@@ -763,14 +784,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;
@@ -780,6 +802,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;
@@ -791,8 +816,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() {
@@ -806,7 +830,7 @@ public class ConversationActivity extends XmppActivity
}
conversation.setMutedTill(till);
ConversationActivity.this.xmppConnectionService.databaseBackend
- .updateConversation(conversation);
+ .updateConversation(conversation);
updateConversationList();
ConversationActivity.this.mConversationFragment.updateMessages();
invalidateOptionsMenu();
@@ -944,18 +968,23 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.reInit(getSelectedConversation());
}
- for(Iterator i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
- attachImageToConversation(getSelectedConversation(),i.next());
- }
+ if(!forbidProcessingPendings) {
+ for (Iterator i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
+ Uri foo = i.next();
+ attachImageToConversation(getSelectedConversation(), foo);
+ }
- for(Iterator i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
- attachFileToConversation(getSelectedConversation(),i.next());
- }
+ for (Iterator 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());
}
@@ -965,10 +994,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);
}
@@ -1065,6 +1105,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();
@@ -1205,10 +1248,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);
}
@@ -1217,6 +1256,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();
@@ -1238,6 +1301,7 @@ public class ConversationActivity extends XmppActivity
ConversationActivity.this.mConversationFragment.updateMessages();
updateActionBarTitle();
}
+ invalidateOptionsMenu();
}
@Override
@@ -1258,12 +1322,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 d254ece7..de758efc 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;
@@ -46,12 +47,12 @@ import eu.siacs.conversations.crypto.PgpEngine;
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.TransferablePlaceholder;
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;
@@ -292,19 +293,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);
}
}
@@ -315,7 +324,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
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));
@@ -323,6 +332,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;
@@ -377,19 +389,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);
}
}
@@ -402,7 +415,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 {
@@ -563,7 +583,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) {
@@ -817,7 +837,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} catch (final NoSuchElementException ignored) {
}
- activity.xmppConnectionService.updateConversationUi();
+ activity.refreshUi();
}
});
}
@@ -1120,6 +1140,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;
@@ -1182,4 +1209,19 @@ 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());
+ }
+ }
+ }
+
}
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();
}
};
+
+ 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() {
- 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);
- }
- }
- });
+ refreshUi();
}
private final UiCallback mAvatarFetchCallback = new UiCallback() {
@@ -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 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 eb5d9b2e..6e5fe610 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -1,19 +1,6 @@
package eu.siacs.conversations.ui;
-import java.security.KeyStoreException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Locale;
-
-import de.duenndns.ssl.MemorizingTrustManager;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.XmppConnection;
-
import android.app.AlertDialog;
-import android.app.Fragment;
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.SharedPreferences;
@@ -25,6 +12,17 @@ import android.preference.Preference;
import android.preference.PreferenceManager;
import android.widget.Toast;
+import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener {
private SettingsFragment mSettingsFragment;
@@ -182,4 +180,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..cf22416f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
@@ -0,0 +1,255 @@
+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 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 ownKeysToTrust = new HashMap<>();
+ private final Map 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);
+
+ 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 {
+ lockOrUnlockAsNeeded();
+ setDone();
+ }
+ }
+
+ private void getFingerprints(final Account account) {
+ Set ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
+ Set 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 7c994c31..7734dc11 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;
@@ -65,6 +70,7 @@ import java.util.concurrent.RejectedExecutionException;
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;
@@ -74,7 +80,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;
@@ -90,6 +99,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;
@@ -116,7 +126,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);
@@ -128,9 +138,7 @@ public abstract class XmppActivity extends Activity {
}
}
- protected void refreshUiReal() {
-
- };
+ abstract protected void refreshUiReal();
protected interface OnValueEdited {
public void onValueEdited(String value);
@@ -287,6 +295,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() {
@@ -308,6 +319,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.removeOnShowErrorToastListener();
}
+ if (this instanceof OnKeyStatusUpdated) {
+ this.xmppConnectionService.removeOnNewKeysAvailableListener();
+ }
}
@Override
@@ -336,7 +350,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);
@@ -371,14 +386,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);
}
- private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) {
+ public void privateMsgInMuc(Conversation conversation, String nick) {
+ switchToConversation(conversation, null, nick, true, false);
+ }
+
+ 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);
@@ -389,6 +408,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) {
@@ -404,10 +424,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);
}
@@ -588,6 +613,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();
@@ -707,6 +850,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..98250af9 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;
@@ -14,7 +8,15 @@ import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
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 {
@@ -34,7 +36,11 @@ public class AccountAdapter extends ArrayAdapter {
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 +59,7 @@ public class AccountAdapter extends ArrayAdapter {
}
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 bfe44326..6918713e 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -21,8 +21,8 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
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 {
private ArrayList domains;
private Filter domainFilter = new Filter() {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
index 7b20b55f..ad7d7622 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java
@@ -1,15 +1,5 @@
package eu.siacs.conversations.ui.adapter;
-import java.lang.ref.WeakReference;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.ui.XmppActivity;
-import eu.siacs.conversations.utils.UIHelper;
-import eu.siacs.conversations.xmpp.jid.Jid;
-
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
@@ -26,6 +16,16 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
public class ListItemAdapter extends ArrayAdapter {
protected XmppActivity activity;
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 167f3f02..1ddd6c44 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.text.Spannable;
@@ -26,13 +27,14 @@ import android.widget.Toast;
import java.util.List;
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;
@@ -79,18 +81,30 @@ public class MessageAdapter extends ArrayAdapter {
return 3;
}
+ public int getItemViewType(Message message) {
+ if (message.getType() == Message.TYPE_STATUS) {
+ return STATUS;
+ } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
+ return RECEIVED;
+ }
+
+ return SENT;
+ }
+
@Override
public int getItemViewType(int position) {
- if (getItem(position).getType() == Message.TYPE_STATUS) {
- return STATUS;
- } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
- return RECEIVED;
+ 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;
@@ -145,15 +159,39 @@ public class MessageAdapter extends ArrayAdapter {
}
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(),
@@ -185,19 +223,19 @@ public class MessageAdapter extends ArrayAdapter {
}
}
- 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);
}
@@ -205,7 +243,7 @@ public class MessageAdapter extends ArrayAdapter {
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);
}
@@ -223,7 +261,7 @@ public class MessageAdapter extends ArrayAdapter {
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);
}
@@ -265,8 +303,7 @@ public class MessageAdapter extends ArrayAdapter {
}
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(),
@@ -281,7 +318,7 @@ public class MessageAdapter extends ArrayAdapter {
} 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);
}
@@ -350,17 +387,15 @@ public class MessageAdapter extends ArrayAdapter {
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);
@@ -489,7 +524,7 @@ public class MessageAdapter extends ArrayAdapter {
} 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);
@@ -501,10 +536,9 @@ public class MessageAdapter extends ArrayAdapter {
}
} 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() {
@@ -517,7 +551,7 @@ public class MessageAdapter extends ArrayAdapter {
}
}
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
- displayDecryptionFailed(viewHolder);
+ displayDecryptionFailed(viewHolder,type);
} else {
if (GeoHelper.isGeoUri(message.getBody())) {
displayLocationMessage(viewHolder,message);
@@ -526,11 +560,19 @@ public class MessageAdapter extends ArrayAdapter {
} 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);
}
}
- displayStatus(viewHolder, message);
+ 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, type);
return view;
}
@@ -543,7 +585,7 @@ public class MessageAdapter extends ArrayAdapter {
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= 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/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 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..b90f06ff 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 phoneContacts, final OnPhoneContactsLoadedListener listener) {
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 2e768ad9..cac23f07 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -1,5 +1,10 @@
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.util.Calendar;
@@ -9,15 +14,10 @@ import java.util.Locale;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.xmpp.jid.Jid;
-import android.content.Context;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-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..dc5a68f6 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);
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,19 +1,19 @@
package eu.siacs.conversations.xml;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import eu.siacs.conversations.Config;
-
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 java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import eu.siacs.conversations.Config;
+
public class XmlReader {
private XmlPullParser parser;
private PowerManager.WakeLock wakeLock;
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..5ac08976 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -26,7 +26,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,6 +34,7 @@ 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;
@@ -81,7 +81,6 @@ 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;
@@ -95,7 +94,7 @@ public class XmppConnection implements Runnable {
private String streamId = null;
private int smVersion = 3;
- private final SparseArray messageReceipts = new SparseArray<>();
+ private final SparseArray mStanzaReceipts = new SparseArray<>();
private int stanzasReceived = 0;
private int stanzasSent = 0;
@@ -123,7 +122,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) {
@@ -165,6 +163,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 values = result.getParcelableArrayList("values");
if ("timeout".equals(result.getString("error"))) {
throw new IOException("timeout in dns");
@@ -338,23 +339,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 failedIqPackets = new ArrayList<>();
+ for(int i = 0; i < this.mStanzaReceipts.size(); ++i) {
+ String id = mStanzaReceipts.valueAt(i);
+ Pair 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 +370,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 +398,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);
@@ -521,14 +529,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 {
@@ -707,6 +707,7 @@ public class XmppConnection implements Runnable {
} catch (final InterruptedException ignored) {
}
}
+ clearIqCallbacks();
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
.addChild("resource").setContent(account.getResource());
@@ -737,9 +738,20 @@ public class XmppConnection implements Runnable {
});
}
+ private void clearIqCallbacks() {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": clearing iq iq callbacks");
+ final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.ERROR);
+ Iterator>> iterator = this.packetCallbacks.entrySet().iterator();
+ while(iterator.hasNext()) {
+ Entry> entry = iterator.next();
+ entry.getValue().second.onIqPacketReceived(account,failurePacket);
+ iterator.remove();
+ }
+ }
+
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) {
@@ -763,7 +775,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;
@@ -895,7 +907,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,9 +917,6 @@ public class XmppConnection implements Runnable {
packet.setAttribute("id", id);
}
if (callback != null) {
- if (packet.getId() == null) {
- packet.setId(nextRandomId());
- }
packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
}
this.sendPacket(packet);
@@ -932,11 +941,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 +1014,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 65cafe79..7b140842 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,15 +68,19 @@ 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
@@ -84,15 +97,13 @@ public class JingleConnection implements Transferable {
public void onFileTransmitted(DownloadableFile file) {
if (responder.equals(account.getJid())) {
sendSuccess();
- if (acceptedAutomatically) {
- message.markUnread();
- JingleConnection.this.mXmppConnectionService
- .getNotificationService().push(message);
- }
mXmppConnectionService.getFileBackend().updateFileParams(message);
mXmppConnectionService.databaseBackend.createMessage(message);
- mXmppConnectionService.markMessage(message,
- Message.STATUS_RECEIVED);
+ mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
+ if (acceptedAutomatically) {
+ message.markUnread();
+ JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ }
} 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) {
@@ -319,10 +355,8 @@ public class JingleConnection implements Transferable {
message.setBody(Long.toString(size));
conversation.add(message);
mXmppConnectionService.updateConversationUi();
- if (size < this.mJingleConnectionManager
- .getAutoAcceptFileSize()) {
- Log.d(Config.LOGTAG, "auto accepting file from "
- + packet.getFrom());
+ if (size < this.mJingleConnectionManager.getAutoAcceptFileSize()) {
+ Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
this.acceptedAutomatically = true;
this.sendAccept();
} else {
@@ -333,22 +367,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 +412,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 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());
@@ -748,6 +812,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 +967,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 +1019,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..ab564480 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,18 @@
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..ab7ab73b 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;
@@ -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..398102e1 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
@@ -39,6 +39,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
new file mode 100644
index 00000000..58bf9721
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_action_done.png differ
diff --git a/src/main/res/drawable-hdpi/ic_done_black_24dp.png b/src/main/res/drawable-hdpi/ic_done_black_24dp.png
new file mode 100644
index 00000000..d4c06072
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_done_black_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png
new file mode 100644
index 00000000..51cc4dbd
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_secure_indicator.png b/src/main/res/drawable-hdpi/ic_secure_indicator.png
index 2a2934fb..220463fc 100644
Binary files a/src/main/res/drawable-hdpi/ic_secure_indicator.png and b/src/main/res/drawable-hdpi/ic_secure_indicator.png differ
diff --git a/src/main/res/drawable-hdpi/ic_secure_indicator_white.png b/src/main/res/drawable-hdpi/ic_secure_indicator_white.png
new file mode 100644
index 00000000..46eb1195
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_secure_indicator_white.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_received.9.png b/src/main/res/drawable-hdpi/message_bubble_received.9.png
new file mode 100644
index 00000000..1a496771
Binary files /dev/null and b/src/main/res/drawable-hdpi/message_bubble_received.9.png differ
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
new file mode 100644
index 00000000..9cd2f825
Binary files /dev/null and b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_sent.9.png b/src/main/res/drawable-hdpi/message_bubble_sent.9.png
new file mode 100644
index 00000000..2bb4e04f
Binary files /dev/null and b/src/main/res/drawable-hdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-hdpi/switch_thumb_disable.png b/src/main/res/drawable-hdpi/switch_thumb_disable.png
new file mode 100644
index 00000000..1e9b151b
Binary files /dev/null and b/src/main/res/drawable-hdpi/switch_thumb_disable.png differ
diff --git a/src/main/res/drawable-hdpi/switch_thumb_off_normal.png b/src/main/res/drawable-hdpi/switch_thumb_off_normal.png
new file mode 100644
index 00000000..b7c1fc11
Binary files /dev/null and b/src/main/res/drawable-hdpi/switch_thumb_off_normal.png differ
diff --git a/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png
new file mode 100644
index 00000000..ca6e4909
Binary files /dev/null and b/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png differ
diff --git a/src/main/res/drawable-hdpi/switch_thumb_on_normal.png b/src/main/res/drawable-hdpi/switch_thumb_on_normal.png
new file mode 100644
index 00000000..cbcda5d4
Binary files /dev/null and b/src/main/res/drawable-hdpi/switch_thumb_on_normal.png differ
diff --git a/src/main/res/drawable-hdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-hdpi/switch_thumb_on_pressed.png
new file mode 100644
index 00000000..234b12dc
Binary files /dev/null and b/src/main/res/drawable-hdpi/switch_thumb_on_pressed.png differ
diff --git a/src/main/res/drawable-mdpi/ic_action_done.png b/src/main/res/drawable-mdpi/ic_action_done.png
new file mode 100644
index 00000000..cf5fab3a
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_action_done.png differ
diff --git a/src/main/res/drawable-mdpi/ic_done_black_24dp.png b/src/main/res/drawable-mdpi/ic_done_black_24dp.png
new file mode 100644
index 00000000..5e5e7cf2
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_done_black_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png
new file mode 100644
index 00000000..c136c59f
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_secure_indicator.png b/src/main/res/drawable-mdpi/ic_secure_indicator.png
index 5a73aef4..690d4d03 100644
Binary files a/src/main/res/drawable-mdpi/ic_secure_indicator.png and b/src/main/res/drawable-mdpi/ic_secure_indicator.png differ
diff --git a/src/main/res/drawable-mdpi/ic_secure_indicator_white.png b/src/main/res/drawable-mdpi/ic_secure_indicator_white.png
new file mode 100644
index 00000000..e2f894ef
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_secure_indicator_white.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_received.9.png b/src/main/res/drawable-mdpi/message_bubble_received.9.png
new file mode 100644
index 00000000..7e134402
Binary files /dev/null and b/src/main/res/drawable-mdpi/message_bubble_received.9.png differ
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
new file mode 100644
index 00000000..38f75e05
Binary files /dev/null and b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_sent.9.png b/src/main/res/drawable-mdpi/message_bubble_sent.9.png
new file mode 100644
index 00000000..b42664a8
Binary files /dev/null and b/src/main/res/drawable-mdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-mdpi/switch_thumb_disable.png b/src/main/res/drawable-mdpi/switch_thumb_disable.png
new file mode 100644
index 00000000..968de345
Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_thumb_disable.png differ
diff --git a/src/main/res/drawable-mdpi/switch_thumb_off_normal.png b/src/main/res/drawable-mdpi/switch_thumb_off_normal.png
new file mode 100644
index 00000000..51fb4d7a
Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_thumb_off_normal.png differ
diff --git a/src/main/res/drawable-mdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-mdpi/switch_thumb_off_pressed.png
new file mode 100644
index 00000000..ca788445
Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_thumb_off_pressed.png differ
diff --git a/src/main/res/drawable-mdpi/switch_thumb_on_normal.png b/src/main/res/drawable-mdpi/switch_thumb_on_normal.png
new file mode 100644
index 00000000..6a93d5f7
Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_thumb_on_normal.png differ
diff --git a/src/main/res/drawable-mdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-mdpi/switch_thumb_on_pressed.png
new file mode 100644
index 00000000..e8d7bd0f
Binary files /dev/null and b/src/main/res/drawable-mdpi/switch_thumb_on_pressed.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_action_done.png b/src/main/res/drawable-xhdpi/ic_action_done.png
new file mode 100644
index 00000000..b8915716
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_action_done.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xhdpi/ic_done_black_24dp.png
new file mode 100644
index 00000000..64a4944f
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_done_black_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png
new file mode 100644
index 00000000..7891efff
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_secure_indicator.png b/src/main/res/drawable-xhdpi/ic_secure_indicator.png
index 1f4c9a32..cd0d1391 100644
Binary files a/src/main/res/drawable-xhdpi/ic_secure_indicator.png and b/src/main/res/drawable-xhdpi/ic_secure_indicator.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_secure_indicator_white.png b/src/main/res/drawable-xhdpi/ic_secure_indicator_white.png
new file mode 100644
index 00000000..b624a8ce
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_secure_indicator_white.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_received.9.png b/src/main/res/drawable-xhdpi/message_bubble_received.9.png
new file mode 100644
index 00000000..dc8ee534
Binary files /dev/null and b/src/main/res/drawable-xhdpi/message_bubble_received.9.png differ
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
new file mode 100644
index 00000000..b5be808f
Binary files /dev/null and b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png
new file mode 100644
index 00000000..f1c46232
Binary files /dev/null and b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-xhdpi/switch_thumb_disable.png b/src/main/res/drawable-xhdpi/switch_thumb_disable.png
new file mode 100644
index 00000000..7f677324
Binary files /dev/null and b/src/main/res/drawable-xhdpi/switch_thumb_disable.png differ
diff --git a/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png
new file mode 100644
index 00000000..4199d322
Binary files /dev/null and b/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png differ
diff --git a/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png
new file mode 100644
index 00000000..2b86888f
Binary files /dev/null and b/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png differ
diff --git a/src/main/res/drawable-xhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xhdpi/switch_thumb_on_normal.png
new file mode 100644
index 00000000..daa30990
Binary files /dev/null and b/src/main/res/drawable-xhdpi/switch_thumb_on_normal.png differ
diff --git a/src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png
new file mode 100644
index 00000000..6aab47c9
Binary files /dev/null and b/src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png
new file mode 100644
index 00000000..c9c01741
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png
new file mode 100644
index 00000000..9c1e27d7
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_secure_indicator.png b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png
index 1ee9b67d..6a74ccbe 100644
Binary files a/src/main/res/drawable-xxhdpi/ic_secure_indicator.png and b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_secure_indicator_white.png b/src/main/res/drawable-xxhdpi/ic_secure_indicator_white.png
new file mode 100644
index 00000000..4945c959
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_secure_indicator_white.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png
new file mode 100644
index 00000000..6ce84487
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png differ
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
new file mode 100644
index 00000000..a3181869
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png
new file mode 100644
index 00000000..9f36a649
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_disable.png b/src/main/res/drawable-xxhdpi/switch_thumb_disable.png
new file mode 100644
index 00000000..db7c1df4
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/switch_thumb_disable.png differ
diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png
new file mode 100644
index 00000000..f747b559
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png differ
diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png
new file mode 100644
index 00000000..b9fe6d46
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png differ
diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png
new file mode 100644
index 00000000..88199024
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png differ
diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png
new file mode 100644
index 00000000..7a4fed54
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png
new file mode 100644
index 00000000..2f6d6386
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png
new file mode 100644
index 00000000..e44a6d28
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png
new file mode 100644
index 00000000..e7fa6754
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png differ
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
new file mode 100644
index 00000000..398e53a2
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png
new file mode 100644
index 00000000..be428cd2
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png b/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png
new file mode 100644
index 00000000..3970168c
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png differ
diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png
new file mode 100644
index 00000000..ea8d4f89
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png differ
diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png
new file mode 100644
index 00000000..84d667b1
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png differ
diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png
new file mode 100644
index 00000000..06b190eb
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png differ
diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png
new file mode 100644
index 00000000..79c30d1e
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png differ
diff --git a/src/main/res/drawable/message_border.xml b/src/main/res/drawable/account_image_border.xml
similarity index 100%
rename from src/main/res/drawable/message_border.xml
rename to 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 @@
+
+
+
+
-
+
+
+
+
+ -
+
+
+
+
+
+
\ 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 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..cac9a9fa 100644
--- a/src/main/res/layout/account_row.xml
+++ b/src/main/res/layout/account_row.xml
@@ -1,20 +1,21 @@
+ 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:paddingLeft="8dp"
+ android:paddingBottom="8dp"
+ android:paddingTop="8dp">
-
-
+ android:contentDescription="@string/account_image_description"
+ app:riv_corner_radius="2dp" />
-
\ No newline at end of file
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"/>
diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml
index 98de84f5..0be934a8 100644
--- a/src/main/res/layout/activity_edit_account.xml
+++ b/src/main/res/layout/activity_edit_account.xml
@@ -1,9 +1,10 @@
+ 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:contentDescription="@string/account_image_description"
+ app:riv_corner_radius="2dp"/>
+
+
+
+
+
+
+ android:typeface="monospace"
+ android:fontFamily="monospace"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml
index b89c9945..6edbb706 100644
--- a/src/main/res/layout/activity_muc_details.xml
+++ b/src/main/res/layout/activity_muc_details.xml
@@ -1,8 +1,9 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@color/grey200">
-
-
+ android:src="@drawable/ic_profile"
+ app:riv_corner_radius="2dp" />
+ android:background="@drawable/account_image_border" >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"/>
+ android:typeface="monospace"
+ android:fontFamily="monospace"/>
+ 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">
-
-
+ android:src="@drawable/ic_profile"
+ app:riv_corner_radius="2dp" />
diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml
index 933b72b4..a4fd29e9 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" >
-
+ android:paddingTop="8dp"
+ android:paddingLeft="8dp"
+ android:paddingBottom="8dp">
+ android:typeface="monospace"
+ android:fontFamily="monospace"/>
+
+
-
+ android:visibility="gone" />
+
+
+
+
\ 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 28e526e0..15fa60af 100644
--- a/src/main/res/layout/conversation_list_row.xml
+++ b/src/main/res/layout/conversation_list_row.xml
@@ -1,7 +1,8 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:descendantFocusability="blocksDescendants">
-
+ android:scaleType="centerCrop"
+ app:riv_corner_radius="2dp" />
+ tools:listitem="@layout/message_sent">
+ xmlns:app="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">
+
+
+ android:padding="2dp">
-
-
\ 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 f8365ad3..e3b38cd9 100644
--- a/src/main/res/layout/message_sent.xml
+++ b/src/main/res/layout/message_sent.xml
@@ -1,12 +1,25 @@
+ xmlns:app="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">
+
+
+ android:padding="2dp">
-
-
-
-
\ 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 @@
+ 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">
-
+ android:src="@drawable/ic_profile"
+ app:riv_corner_radius="1dp"/>
+
+
\ 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
@@ -5,6 +5,9 @@
+
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 @@
-
+
+
+
-
+
-
+
+ android:visible="false"/>
-
-
-
+
+
-
\ No newline at end of file
+ android:visible="false"/>
+
diff --git a/src/main/res/values-ar-rEG/strings.xml b/src/main/res/values-ar-rEG/strings.xml
index c5a43674..0d85e55c 100644
--- a/src/main/res/values-ar-rEG/strings.xml
+++ b/src/main/res/values-ar-rEG/strings.xml
@@ -104,7 +104,6 @@
تفعيل الاهتزاز عندما تصل رساله جديده
التبيه الصوتي
سماع صوت عندما تصل رساله
- الاشعارات من الغرف
اعدادات متقدمّة
لا ترسل تقارير أخطاء
الغاء ارسال تقارير الأخطاء يقلل من فرص حل المشكلة سريعا فكن متعاون
diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml
index cff228f7..17c9061c 100644
--- a/src/main/res/values-bg/strings.xml
+++ b/src/main/res/values-bg/strings.xml
@@ -105,8 +105,6 @@
Също така да има и вибрация при получаване на ново съобщение
Звук
Изпълнение на звук с известието
- Известия за беседите
- Известяване винаги, когато пристигне ново съобщение в беседа, а не само когато тя е отбелязана
Продължителност на отсрочване на известията
Изключва известията за кратко, след като бъде получено копие на съобщение
Разширени настройки
@@ -427,7 +425,6 @@
Получено местоположение
Conversation се затвори
Напуснахте беседата
- Настройки на сертификата
Да не се вярва на системните сертификати
Всички сертификати трябва да бъдат одобрени на ръка
Премахване на сертификатите
diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml
index b10fb617..74d87736 100644
--- a/src/main/res/values-ca/strings.xml
+++ b/src/main/res/values-ca/strings.xml
@@ -105,8 +105,6 @@
Vibra quan arriba un nou missatge
So
Reprodueix el to de trucada amb la notificació
- Notificacions de conferència
- Sempre notifica quan arriba un nou missatge de conferència en comptes de només quan està destacat
Notificació del període d\'espera
Desactiva les notificacions durant un breu termini després de rebre una còpia de missatges carbon
Opcions avançades
@@ -424,7 +422,6 @@
Localització rebuda
Conversa tancada
S\'ha sortit de la conferència
- Opcions de certificats
No confiar en les CAs del sistema
Tots els certificats han de ser aprovats manualment
Eliminar certificats
diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml
index 190fd575..6463d763 100644
--- a/src/main/res/values-cs/strings.xml
+++ b/src/main/res/values-cs/strings.xml
@@ -105,8 +105,6 @@
Vibrovat při přijetí nové zprávy
Zvuk
Přehrát zvuk společně s upozorněním
- Upozornění při konferencích
- Vždy upozorňovat při nové konferenční zprávě, nejen pokud je vybrána
Četnost upozornění
Neupozorňovat krátce poté co byla obdržena kopie zprávy
Pokročilé nastavení
@@ -427,7 +425,6 @@
Přijmout pozici
Conversation zavřena
Opustil(a) konferenci
- Nastavení certifikátu
Nedůvěřovat systémovým CA
Všechny certifikáty musí být schváleny ručně
Odstranit certifikáty
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml
index efe00a4f..8c77d0a8 100644
--- a/src/main/res/values-de/strings.xml
+++ b/src/main/res/values-de/strings.xml
@@ -105,8 +105,6 @@
Vibriere, wenn eine neue Nachricht ankommt
Klingelton
Spiele Klingelton, wenn eine neue Nachricht ankommt
- Konferenz-Benachrichtigungen
- Benachrichtige mich bei jeder Konferenz-Nachricht und nicht nur, wenn ich angesprochen werde
Gnadenfrist
Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.
Erweiterte Optionen
@@ -427,7 +425,6 @@
Standort empfangen
Unterhaltung beendet
Konferenz verlassen
- Zertifikats-Optionen
Misstraue Zertifizierungsstellen
Alle Zertifikate müssen manuell bestätigt werden
Zertifikate löschen
diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml
index d796cd6d..b9fc352c 100644
--- a/src/main/res/values-el/strings.xml
+++ b/src/main/res/values-el/strings.xml
@@ -105,8 +105,6 @@
Επίσης δόνηση όταν έρχεται ένα νέο μήνυμα
Ήχος
Αναπαραγωγή ήχου κλήσης με την ειδοποίηση
- Ειδοποιήσεις συνδιασκέψεων
- Ειδοποίηση πάντα όταν ένα νέο μήνυμα συνδιάσκεψης λαμβάνεται αντί για μόνο όταν τονίζεται
Περίοδος χάριτος ειδοποιήσεων
Απενεργοποίηση ειδοποιήσεων για λίγο χρόνο μετά από τη λήψη ακριβούς αντιγράφου
Προχωρημένες επιλογές
@@ -424,7 +422,6 @@
Ελήφθη τοποθεσία
Η συζήτηση έκλεισε
Έφυγε από την συνδιάσκεψη
- Επιλογές πιστοποιητικών
Μη έμπιστες αρχές πιστοποίησης συστήματος
Όλα τα πιστοποιητικά πρέπει να εγκριθούν χειροκίνητα
Αφαίρεση πιστοποιητικών
diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml
index 29938612..f1b5d54c 100644
--- a/src/main/res/values-es/strings.xml
+++ b/src/main/res/values-es/strings.xml
@@ -105,14 +105,12 @@
Vibra cuando llega un nuevo mensaje
Sonido
Reproduce tono con la notificación
- Notif. conversación grupo
- Siempre notifica cuando llega un mensaje a una conversación en grupo y no solo cuando alguien menciona tu nombre en un mensaje
Notificaciones Carbons
Deshabilita las notificaciones durante un corto periodo de tiempo después de recibir la copia del mensaje carbon
Opciones avanzadas
Nunca informar de errores
Si envías registros de error ayudas al desarrollo de Conversations
- Confirmar Mensajes
+ Confirmar mensajes
Permitir a tus contactos saber cuando recibes y lees un mensaje
Opciones de interfaz
OpenKeychain reportó un error
@@ -427,13 +425,12 @@
Ubicación recibida
Conversación cerrada
Has salido de la conversación
- Opciones de Certificados
No confiar en los CAs del sistema
Todos los certificados deben ser aprobados manualmente
- Eliminar Certificados
+ Eliminar certificados
Eliminar manualmente certificados aceptados
No aceptar certificados manualmente
- Eliminar Certificados
+ Eliminar certificados
Eliminar seleccionados
Cancelar
@@ -445,7 +442,7 @@
- Seleccionados %d contactos
Cambiar el botón de enviar por botón de acción rápida
- Acción Rápida
+ Acción rápida
Ninguna
Usada más recientemente
Elegir acción rápida
diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml
index 028acc53..0bb59671 100644
--- a/src/main/res/values-eu/strings.xml
+++ b/src/main/res/values-eu/strings.xml
@@ -105,8 +105,6 @@
Dardaratu ere mezu berri bat heltzerakoan
Soinua
Dei-tonua jo jakinarazpenarekin
- Konferentzien jakinarazpenak
- Beti jakinarazi konferentzia mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan
Jakinarazpenen grazia epea
Jakinarazpenak denbora labur baterako ezgaitu ikatz-kopia bat jaso ondoren
Aukera aurreratuak
@@ -427,7 +425,6 @@
Kokapena jaso da
Elkarrizketa itxi egin da
Konferentzia utzi egin da
- Ziurtagirien aukerak
Sistemaren CAtaz ez fidatu
Ziurtagiri guztiak eskuz onartu behar dira
Ziurtagiriak kendu
diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml
index 05ac82ac..646f5414 100644
--- a/src/main/res/values-fr/strings.xml
+++ b/src/main/res/values-fr/strings.xml
@@ -105,8 +105,6 @@
Vibrer lors de l\'arrivée d\'un message
Sonore
Jouer une sonnerie lors de l\'arrivée d\'un message
- Notifications lors des conférences
- Toujours notifier l\'arrivée d\'un message provenant d\'une conférence.
Période sans notification
Désactiver momentanément les notifications après l\'arrivée d\'une copie carbone.
Options avancées
diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml
index 477b7580..6a6b23c1 100644
--- a/src/main/res/values-gl/strings.xml
+++ b/src/main/res/values-gl/strings.xml
@@ -69,8 +69,6 @@
Treme cando chega unha novo mensaxe
Son
Reproduce un ton ca notificación
- Notificacións de conferencia
- Siempre notifica cuando chega unha mensaxe de conferencia e non solo cuando chega unha mensaxe destacada
Notificacións Carbons
Deshabilita as notificacións durante un corto periodo de tiempo despois de recibir a copia da mensaxe carbón
Opcións avanzadas
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 @@
Juga aktifkan getaran bila pesan baru tiba
Suara
mainkan suara saat menerima notifikasi
- Notifikasi Conference
- Selalu memberitahukan bila pesan conference baru diterima daripada hanya dicetak tebal
Tenggang waktu pemberitahuan
Nonaktifkan pemberitahuan untuk waktu yang singkat setelah salinan diterima
Opsi Lanjutan
@@ -424,7 +422,6 @@
Lokasi yang diterima
Percakapan tertutup
Tinggalkan conference
- Opsi Sertifikat
Jangan percaya sistem CA
Semua sertifikat harus disetujui secara manual
Hapus sertifikat
diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml
index 01299530..372b87e2 100644
--- a/src/main/res/values-it/strings.xml
+++ b/src/main/res/values-it/strings.xml
@@ -60,7 +60,7 @@
Errore di Conversations
Se scegli di inviare una segnalazione dell’errore aiuterai lo sviluppo di Conversations\nAttenzione: Questo utilizzerà il tuo account XMPP per inviare la segnalazione agli sviluppatori.
Invia adesso
- Non chiedere mai più
+ Non chiedere più
Impossibile collegarsi tramite questo utente
Impossibile collegarsi tramite più utenti
Tocca qui per gestire i tuoi utenti
@@ -105,8 +105,6 @@
Vibra anche quando arriva un nuovo messaggio
Suono
Riproduci una suoneria con la notifica
- Notifiche Conferenze
- Notifica sempre quando arriva un nuovo messaggio da una conferenza, invece che solo quando in primo piano
Periodo tra notifiche
Disabilita le notifiche per un breve lasso di tempo dopo che un messaggio è stato ricevuto
Opzioni Avanzate
@@ -293,6 +291,7 @@
Copia testo
Copia URL originale
Invia di nuovo
+ URL del file
Messaggio di testo
URL copiato
Messaggio copiato
@@ -406,6 +405,7 @@
Applicazione Android
Contatto
Ricevuto %s
+ Disabilita i servizi in background
Tocca per avviare Conversations
Il tuo avatar è stato pubblicato!
Invio %s
@@ -422,7 +422,6 @@
Posizione ricevuta
Conversazione interrotta
Conferenza terminata
- Opzioni per i certificati
Non ti fidare delle CA di sistema
Tutti i certificati devono essere accettati manualmente
Elimina i certificati
@@ -439,4 +438,8 @@
- Seleziona il %d contatto
- Selezionati %d contatti
+ Nessuno
+ Usati recentemente
+ File non trovato sul server remoto
+ Cerca contatti o gruppi
diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml
index 545ba1c8..8eeaab6b 100644
--- a/src/main/res/values-iw/strings.xml
+++ b/src/main/res/values-iw/strings.xml
@@ -90,8 +90,6 @@
הרטט גם כאשר הודעה חדשה מגיעה
צליל
נגן צלצול עם התראה
- התראות ועידה
- תמיד תודיע כאשר הודעת ועידה חדשה מגיעה במקום רק כאשר מודגשת
משך ארכת התראה
נטרל התראות לזמן קצר לאחר שהודעת פחם התקבלה
אפשרויות מתקדמות
diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml
index 35347734..35ccc7dd 100644
--- a/src/main/res/values-ja/strings.xml
+++ b/src/main/res/values-ja/strings.xml
@@ -105,8 +105,6 @@
新しいメッセージが到着したときに振動もします
サウンド
通知で着信音を再生します
- 会議通知
- 新しい会議メッセージが到着したとき、ハイライト表示ではなく、常に通知します
通知猶予期間
カーボンコピーを受信した後、短時間、通知を無効にします
詳細オプション
@@ -427,7 +425,6 @@
位置を受信しました
会話が閉じられました
退出した会話
- 証明書オプション
システムの CA を信頼しない
すべての証明書を手動で承認する必要があります
証明書を削除
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 @@
새 메세지 도착시 진동
소리
알림과 동시에 벨소리 재생
- 회의 알림
- 새 회의 메세지가 도착시, 강조됐을 때 뿐만 아니라 항상 알림
알림 유예
Carbon Copy 수신 후에 잠시동안 알림 해제
추가 설정
@@ -424,7 +422,6 @@
위치 수신
대화 끝남
회의에서 나감
- 인증 설정
시스템 CA를 신뢰하지 않음
모든 인증서는 수동으로 승인되어야 함
인증서 삭제
diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml
index 80b6934b..a4ef81ab 100644
--- a/src/main/res/values-nl/strings.xml
+++ b/src/main/res/values-nl/strings.xml
@@ -105,8 +105,6 @@
Tril ook wanneer een nieuw bericht arriveert
Geluid
Speel ringtone af bij melding
- Groepsgespreksmeldingen
- Toon altijd meldingen als er nieuwe berichten arriveren in groepsgesprekken in plaats van alleen wanneer gemarkeerd
Uitstelperiode voor meldingen
Zet meldingen voor korte tijd uit als er een carbon copy wordt ontvangen
Geavanceerde instellingen
@@ -288,6 +286,7 @@
Dit groepsgesprek is enkel voor leden
Je bent uit dit groepsgesprek geschopt
account %s gebruiken
+ %s op HTTP host nakijken
Je bent niet verbonden. Probeer later opnieuw
Bekijk bestandsgrootte van %s
Berichtopties
@@ -417,7 +416,7 @@
Account uitzetten
%s is aan het typen...
%s is gestopt met typen
- Type-meldingen
+ Aan het typen meldingen
Laat je contacten weten wanneer je een nieuw bericht aan het schrijven bent
Locatie versturen
Locatie weergeven
@@ -426,7 +425,6 @@
Locatie ontvangen
Gesprek gesloten
Groepsgesprek verlaten
- Certificaatopties
Vertrouw geen systeem-CA\'s.
Alle certificaten moeten handmatig goedgekeurd worden
Verwijder certificaten
@@ -449,4 +447,5 @@
Recent gebruikt
Kies snelle actie
Bestand niet gevonden op externe server
+ Zoeken naar contacten of groepen
diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml
index ea2ecc52..fb94c605 100644
--- a/src/main/res/values-pl/strings.xml
+++ b/src/main/res/values-pl/strings.xml
@@ -105,8 +105,6 @@
Wibruj, gdy nadejdzie wiadomość
Dźwięk
Odtwórz dźwięk z powiadomieniem
- Powiadomienia konferencji
- Zawsze powiadamiaj o nowej wiadomości w konferencji
Opóźnienie powiadomień
Wyłącz powiadomienia przez krótki czas po otrzymaniu kopii wiadomości
Opcje zaawansowane
@@ -424,7 +422,6 @@
Otrzymano lokalizację
Zamknięto konwersację
Opuszczono konferencję
- Ustawienia certyfikatów
Nie ufaj certyfikatom systemowym
Wymagaj ręcznego potwierdzania certyfikatów
Usuń certyfikat
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 @@
Vibrar também quando uma nova mensagem for recebida
Som
Tocar um som com a notificação
- Notificações de conferência
- Sempre notificar quando uma nova mensagem de conferencia for recebida ao invés de apenas quando a mesma for ressaltada
Período de carência da notificação
Desativar notificações por um curto período após a copia oculta ser recebida
Opções avançadas
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml
index 56a87ffc..053a675c 100644
--- a/src/main/res/values-ru/strings.xml
+++ b/src/main/res/values-ru/strings.xml
@@ -105,8 +105,6 @@
Использовать вибрацию когда приходят новые сообщения
Звуковой сигнал
Выберите звуковой сигнал для сообщений
- Уведомления конференции
- Всегда сообщать при получении нового сообщения в конференции
Отсрочка уведомлений
Не использовать уведомления, если вы прочитали сообщение на другом устройстве
Дополнительные параметры
@@ -424,7 +422,6 @@
Получено местоположение
Беседа окончена
Покинул беседу
- Опции сертификата
Не доверять системным УЦ
Все сертификаты должны быть подтверждены вручную
Удалить сертификат
diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml
index d6238794..6f3bf982 100644
--- a/src/main/res/values-sk/strings.xml
+++ b/src/main/res/values-sk/strings.xml
@@ -105,8 +105,6 @@
Vibrovať pri prijatí novej správy
Zvuk
Prehrať zvuk spolu s upozornením
- Upozornenia pri skupinovej konverzácii
- Vždy upozorniť pri novej konferenčnej správe, nie len ak je zvýraznená
Doba na prečítanie upozornenia
Neupozorňovať krátko po obdržaní kópie správy
Rozšírené možnosti
@@ -426,7 +424,6 @@
Prijatá poloha
Konverzácia zatvorená
Opustil skupinovú konverzáciu
- Možnosti certifikátu
Nedôverovať systému CAs
Všetky certifikáty musia byť ručne schválené
Odstrániť certifikáty
diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml
index 082cfa46..96db8b7e 100644
--- a/src/main/res/values-sr/strings.xml
+++ b/src/main/res/values-sr/strings.xml
@@ -105,8 +105,6 @@
Вибрирај кад стигне нова порука
Звук
Звуци обавештења
- Обавештења конференције
- Увек обавести кад нова порука у конференцији стигне уместо само кад је означена
Период одгоде обавештења
Онемогући обавештења на кратко по примању карбон копије
Напредне поставке
@@ -427,7 +425,6 @@
Примљена локација
Преписка затворена
Напусти конференцију
- Опције сертификата
Не веруј системским сертификационим телима
Сви сертификати морају ручно да се одобре
Уклони сертификате
diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml
index 98ab7110..6c4436a8 100644
--- a/src/main/res/values-sv/strings.xml
+++ b/src/main/res/values-sv/strings.xml
@@ -105,8 +105,6 @@
Vibrera när meddelande tagits emot
Ljud
Spela ljud med notifiering
- Konferensnotifieringar
- Notifiera alltid när nytt konferensmeddelande tagits emot i stället för endast vid highlight
Notifieringsfrist
Inaktivera notifieringar en kort stund efter att en carbon copy tagits emot
Avancerade inställningar
@@ -427,7 +425,6 @@
Mottog position
Konversation stängd
Lämnade konferens
- Certifikatalternativ
Lita inte på systemets CAs
Alla certifikat måste manuellt godkännas
Ta bort certifikat
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 @@
- @color/green700
- @color/accent
+ - true
+ - @color/accent
+
- 12sp
- 14sp
- 20sp
@@ -18,8 +21,10 @@
- @drawable/ic_file_download_white_24dp
- @drawable/ic_edit_white_24dp
- @drawable/ic_edit_grey600_24dp
+ - @drawable/ic_done_black_24dp
- @drawable/ic_group_white_24dp
- @drawable/ic_add_white_24dp
+ - @drawable/ic_refresh_grey600_24dp
- @drawable/ic_attach_file_white_24dp
- @drawable/ic_lock_open_white_24dp
- @drawable/ic_delete_grey600_24dp
diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml
index 0d95ee18..7a67ffdd 100644
--- a/src/main/res/values-zh-rCN/strings.xml
+++ b/src/main/res/values-zh-rCN/strings.xml
@@ -105,8 +105,6 @@
收到新消息时震动
声音
收到新消息时的铃声
- 讨论组通知
- 当有新的消息时总是通知而不是亮屏时才通知
通知限期
接收副本短时间内关闭通知
高级选项
@@ -424,7 +422,6 @@
位置已收到
Conversation 已关闭
离开讨论组
- 证书选项
不相信系统 CA
所有证书必须人工通过
移除证书
diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml
index 1fd7c4bf..8a4d3539 100644
--- a/src/main/res/values-zh-rTW/strings.xml
+++ b/src/main/res/values-zh-rTW/strings.xml
@@ -89,8 +89,6 @@
收到新訊息時震動
聲音
收到新訊息時播放鈴聲
- 群組通知
- 當有新訊息時總是通知,而不是被標記時才通知
通知限期
收到副本後,關閉通知一小段時間
進階選項
diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml
index e314f752..d471e54a 100644
--- a/src/main/res/values/attrs.xml
+++ b/src/main/res/values/attrs.xml
@@ -14,6 +14,7 @@
+
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 @@
#ff0091ea
#de000000
#8a000000
+ #42000000
#1f000000
#ffffffff
#b2ffffff
#fffafafa
#ffeeeeee
+ #ff9e9e9e
#ff424242
#fff44336
+ #ffc62828
#ffff9800
\ No newline at end of file
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index dc29ffd3..0e396491 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -80,6 +80,7 @@
Choose presence to contact
Send plain text message
Send OTR encrypted message
+ Send OMEMO encrypted message
Send OpenPGP encrypted message
Your nickname has been changed
Send unencrypted
@@ -107,8 +108,8 @@
Also vibrate when a new message arrives
Sound
Play ringtone with notification
- Conference notifications
- Always notify when a new conference message arrives instead of only when highlighted
+ Notifications in Public Conferences
+ Always notify when a message arrives in a public conference instead of only when highlighted
Notification grace period
Disable notifications for a short time after a carbon copy was received
Advanced Options
@@ -154,6 +155,7 @@
Plain text
OTR
OpenPGP
+ OMEMO
Edit account
Delete account
Temporarily disable
@@ -189,6 +191,7 @@
XEP-0237: Roster Versioning
XEP-0198: Stream Management
XEP-0163: PEP (Avatars)
+ XEP-xxxx: HTTP File Upload
available
unavailable
Missing public key announcements
@@ -206,6 +209,13 @@
Reception failed
Your fingerprint
OTR fingerprint
+ OMEMO fingerprint
+ OMEMO fingerprint of message
+ Own OMEMO fingerprint
+ Other devices
+ Trust OMEMO Fingerprints
+ Fetching keys...
+ Done
Verify
Decrypt
Conferences
@@ -312,6 +322,7 @@
Conference name
Use room’s subject instead of JID to identify conferences
OTR fingerprint copied to clipboard!
+ OMEMO fingerprint copied to clipboard!
You are banned from this conference
This conference is members only
You have been kicked from this conference
@@ -378,6 +389,14 @@
Reset
Account avatar
Copy OTR fingerprint to clipboard
+ Copy OMEMO fingerprint to clipboard
+ Regenerate OMEMO key
+ Wipe other devices from PEP
+ Clear devices
+ 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.
+ Purge key
+ Are you sure you want to purge this key?
+ It will irreversibly be considered compromised, and you can never build a session with it again.
Fetching history from server
No more history on server
Updating…
@@ -455,7 +474,6 @@
Received location
Conversation closed
Left conference
- Certificate options
Don’t trust system CAs
All certificates must be manually approved
Remove certificates
@@ -477,6 +495,13 @@
None
Most recently used
Choose quick action
- File not found on remote server
Search for contacts or groups
+ Send private message
+ %s has left the conference!
+ Username
+ Username
+ This is not a valid username
+ Download failed: Server not found
+ Download failed: File not found
+ Download failed: Could not connect to host
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 @@
- 1.5dp
- @color/black12
-
\ 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 @@
- @drawable/ic_action_download
- @drawable/ic_action_edit
- @drawable/ic_action_edit_dark
+ - @drawable/ic_action_done
- @drawable/ic_action_group
- @drawable/ic_action_new
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index 5b7d6904..8ab35c09 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -1,12 +1,12 @@
-
+
-
+
+ android:title="@string/pref_grant_presence_updates"/>
+ android:title="@string/pref_xmpp_resource"/>
+ android:title="@string/pref_accept_files"/>
+ android:title="@string/pref_confirm_messages"/>
+ android:title="@string/pref_chat_states"/>
+ android:key="notifications"
+ android:title="@string/pref_notification_settings">
-
+
+ android:title="@string/title_pref_quiet_hours">
+ android:defaultValue="false"
+ android:key="enable_quiet_hours"
+ android:summary="@string/pref_quiet_hours_summary"
+ android:title="@string/title_pref_enable_quiet_hours"/>
+ 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"/>
-
-
+
+
+ android:title="@string/pref_vibrate"/>
+ android:title="@string/pref_sound"/>
+ android:title="@string/pref_conference_notifications"/>
-
+
+ android:title="@string/pref_conference_name"/>
+ android:title="@string/pref_use_larger_font"/>
+ android:title="@string/pref_use_send_button_to_indicate_status"/>
+ android:title="@string/pref_quick_action"/>
+ android:title="@string/pref_show_dynamic_tags"/>
+ android:key="advanced"
+ android:title="@string/pref_advanced_options">
-
-
+ android:title="@string/pref_expert_options">
+
+ android:title="@string/pref_dont_save_encrypted"/>
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ android:title="@string/pref_use_indicate_received"/>
+ android:summary="@string/pref_keep_foreground_service_summary"
+ android:title="@string/pref_keep_foreground_service"/>
@@ -179,9 +172,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"/>
-
+