diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fbbaf833efea260312ab09e6a17d7195c8103eb..644137094ce52b0a24befc99c1206aba15def2b6 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 9e7ffd2fa3d7ee38ff82cd3ac67891891e84a043..a68b162b1d11663d8daa8cf80f9cfb6e3ff9118b 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 0000000000000000000000000000000000000000..efd83c2d00ba30b32a4c4927d64e05162734e793
--- /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 0000000000000000000000000000000000000000..25d1761db7e28bcfb8d87e0d261d750e7ec9f91a
--- /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 0000000000000000000000000000000000000000..002b478156548d6185dc3f097548248ad4ed592e
--- /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 0000000000000000000000000000000000000000..5e8f90f39a2101c26d8fa945bf34df8c2876a019
--- /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 0000000000000000000000000000000000000000..e0331e7b77d6003d1a7c0215a10052822e895506
--- /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 0000000000000000000000000000000000000000..815892ed3b79e610f9e67ed4c5e8912e1cc622e5
--- /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 0000000000000000000000000000000000000000..9353492b5f2b34f589a2cb19b95e67f9f4353c80
--- /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 0000000000000000000000000000000000000000..90ad5091a00f9a46f889a3831b1512d204200f67
--- /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 0000000000000000000000000000000000000000..ca20a5b9458f7156abab997b9cfc19a117c93f4a
--- /dev/null
+++ b/art/omemo_logo.svg
@@ -0,0 +1,273 @@
+
+
+
+
diff --git a/art/render.rb b/art/render.rb
index 698abea5b05f19cdae288881f4e83c1cc09e10c7..dc55c3276cf4f2f33652e48191a680a52cbf159f 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 639e9a7f8e511389b031fcb371e807e24638c3f1..9e1ac0782cd044193fbb3fc2645c99e15c37343b 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 9fe37017155055442d3cc66d757d3f1db70a25fa..15d5bc2555437fff19a52fe771070fcc573f0bca 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 0000000000000000000000000000000000000000..5796ef30e43a1b06a850bfa6c76fbc90898beae9
--- /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 0000000000000000000000000000000000000000..663b42b58a6c886e641bde9d35e35230bf003921
--- /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 0000000000000000000000000000000000000000..3d40a4089bc6a4e37949ead88cca1d2740625758
--- /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 0000000000000000000000000000000000000000..190eb88a87de8709b3057960e5c2951e7bb87830
--- /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 0000000000000000000000000000000000000000..cf950d6da5b3080f21860d1b3e8057004bd8e020
--- /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 0000000000000000000000000000000000000000..c4053854d34d8650fc055c8caeca44da25180201
--- /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 f472361f2beb962e1a2f78fc1bcb06850638f6cc..7a2dc3f7696bb4022d5e1dd93440d0e4c111c014 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 e546f2149fcbfd2c654332491a752b1135eb13c4..f924c05a1c025c65b93bf08599ad9cf64eb9e10a 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 289ed4ea5ceb5c3f24702193a4f2e293fbf68359..9f9f34cf8e59944b97ac40044decf34c053061d6 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -179,13 +179,13 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
- public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) {
+ public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
- && (message.getEncryption() == Message.ENCRYPTION_OTR)) {
+ && (message.getEncryption() == encryptionType)) {
onMessageFound.onMessageFound(message);
- }
+ }
}
}
}
@@ -519,6 +519,13 @@ public class Conversation extends AbstractEntity implements Blockable {
return getContact().getOtrFingerprints().contains(getOtrFingerprint());
}
+ /**
+ * short for is Private and Non-anonymous
+ */
+ public boolean isPnNA() {
+ return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
+ }
+
public synchronized MucOptions getMucOptions() {
if (this.mucOptions == null) {
this.mucOptions = new MucOptions(this);
@@ -542,42 +549,51 @@ public class Conversation extends AbstractEntity implements Blockable {
return this.nextCounterpart;
}
- public int getLatestEncryption() {
- int latestEncryption = this.getLatestMessage().getEncryption();
- if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
- || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
- return Message.ENCRYPTION_PGP;
- } else {
- return latestEncryption;
+ private int getMostRecentlyUsedOutgoingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(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 e;
+ }
+ }
+ }
}
+ return Message.ENCRYPTION_NONE;
}
- public int getNextEncryption(boolean force) {
- int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
- if (next == -1) {
- int latest = this.getLatestEncryption();
- if (latest == Message.ENCRYPTION_NONE) {
- if (force && getMode() == MODE_SINGLE) {
- return Message.ENCRYPTION_OTR;
- } else if (getContact().getPresences().size() == 1) {
- if (getContact().getOtrFingerprints().size() >= 1) {
- return Message.ENCRYPTION_OTR;
+ private int getMostRecentlyUsedIncomingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(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 latest;
+ return e;
}
- } else {
- return latest;
}
- } else {
- return latest;
}
}
- if (next == Message.ENCRYPTION_NONE && force
- && getMode() == MODE_SINGLE) {
- return Message.ENCRYPTION_OTR;
- } else {
- return next;
+ return Message.ENCRYPTION_NONE;
+ }
+
+ public int getNextEncryption() {
+ int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
+ if (next == -1) {
+ int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
+ if (outgoing == Message.ENCRYPTION_NONE) {
+ return this.getMostRecentlyUsedIncomingEncryption();
+ } else {
+ return outgoing;
+ }
}
+ return next;
}
public void setNextEncryption(int encryption) {
diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
index ae9ba1f1d25a0b1ffdd9a79bee5122052848be3d..d35a4b01703cad1e3d9a86f77e964701c6aa96a2 100644
--- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
+++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
@@ -1,28 +1,8 @@
package eu.siacs.conversations.entities;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URLConnection;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.CipherOutputStream;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.utils.MimeUtils;
-import android.util.Log;
+import eu.siacs.conversations.utils.MimeUtils;
public class DownloadableFile extends File {
@@ -30,8 +10,7 @@ public class DownloadableFile extends File {
private long expectedSize = 0;
private String sha1sum;
- private Key aeskey;
- private String mime;
+ private byte[] aeskey;
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
@@ -45,15 +24,7 @@ public class DownloadableFile extends File {
}
public long getExpectedSize() {
- if (this.aeskey != null) {
- if (this.expectedSize == 0) {
- return 0;
- } else {
- return (this.expectedSize / 16 + 1) * 16;
- }
- } else {
- return this.expectedSize;
- }
+ return this.expectedSize;
}
public String getMimeType() {
@@ -79,91 +50,38 @@ public class DownloadableFile extends File {
this.sha1sum = sum;
}
- public void setKey(byte[] key) {
- if (key.length == 48) {
+ public void setKeyAndIv(byte[] keyIvCombo) {
+ if (keyIvCombo.length == 48) {
byte[] secretKey = new byte[32];
byte[] iv = new byte[16];
- System.arraycopy(key, 0, iv, 0, 16);
- System.arraycopy(key, 16, secretKey, 0, 32);
- this.aeskey = new SecretKeySpec(secretKey, "AES");
+ System.arraycopy(keyIvCombo, 0, iv, 0, 16);
+ System.arraycopy(keyIvCombo, 16, secretKey, 0, 32);
+ this.aeskey = secretKey;
this.iv = iv;
- } else if (key.length >= 32) {
+ } else if (keyIvCombo.length >= 32) {
byte[] secretKey = new byte[32];
- System.arraycopy(key, 0, secretKey, 0, 32);
- this.aeskey = new SecretKeySpec(secretKey, "AES");
- } else if (key.length >= 16) {
+ System.arraycopy(keyIvCombo, 0, secretKey, 0, 32);
+ this.aeskey = secretKey;
+ } else if (keyIvCombo.length >= 16) {
byte[] secretKey = new byte[16];
- System.arraycopy(key, 0, secretKey, 0, 16);
- this.aeskey = new SecretKeySpec(secretKey, "AES");
+ System.arraycopy(keyIvCombo, 0, secretKey, 0, 16);
+ this.aeskey = secretKey;
}
}
- public Key getKey() {
- return this.aeskey;
+ public void setKey(byte[] key) {
+ this.aeskey = key;
}
- public InputStream createInputStream() {
- if (this.getKey() == null) {
- try {
- return new FileInputStream(this);
- } catch (FileNotFoundException e) {
- return null;
- }
- } else {
- try {
- IvParameterSpec ips = new IvParameterSpec(iv);
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
- Log.d(Config.LOGTAG, "opening encrypted input stream");
- return new CipherInputStream(new FileInputStream(this), cipher);
- } catch (NoSuchAlgorithmException e) {
- Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
- return null;
- } catch (NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
- return null;
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
- return null;
- } catch (InvalidAlgorithmParameterException e) {
- Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
- return null;
- } catch (FileNotFoundException e) {
- return null;
- }
- }
+ public void setIv(byte[] iv) {
+ this.iv = iv;
}
- public OutputStream createOutputStream() {
- if (this.getKey() == null) {
- try {
- return new FileOutputStream(this);
- } catch (FileNotFoundException e) {
- return null;
- }
- } else {
- try {
- IvParameterSpec ips = new IvParameterSpec(this.iv);
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
- Log.d(Config.LOGTAG, "opening encrypted output stream");
- return new CipherOutputStream(new FileOutputStream(this),
- cipher);
- } catch (NoSuchAlgorithmException e) {
- Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
- return null;
- } catch (NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
- return null;
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
- return null;
- } catch (InvalidAlgorithmParameterException e) {
- Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
- return null;
- } catch (FileNotFoundException e) {
- return null;
- }
- }
+ public byte[] getKey() {
+ return this.aeskey;
+ }
+
+ public byte[] getIv() {
+ return this.iv;
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index 957c2a6d74be57f8aed0da53b2ca420593660012..0eff99cf77ba660110bbe749fda488b7bf34d156 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 d867a370800e8bf843cd4bbbb6472d1dd16484fe..52a862efa4c8d589d52916e350d7be09c7abe687 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 79626511a89919457d5589c963169e800cf3c658..69bb18036ea239730ca82ca4bb5181dec2bef74d 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 47915e3fbe1cd7ac04fb6d994170144a22c22aa2..898d218e4b88d73fe6d3d3c931a44c64631b5a72 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 e698c151de40d8693f94d1106c7bc2b961f3d2ac..a06a0dddbd75a7d57a652d99659e70a7ab37fa4d 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 58a6d1e37046498c86a62723d9dacd86c3868497..90fbadfe3011fa8b18385dbea6c85c1d81a05563 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 62fe4191d6afa3219effacadb180044bcecf62bc..0d202bb92630159f10ccc017f46ce45218c5545f 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);
- }
- connection.connect();
- BufferedInputStream is = new BufferedInputStream(connection.getInputStream());
- file.getParentFile().mkdirs();
- file.createNewFile();
- OutputStream os = file.createOutputStream();
- if (os == null) {
- throw new IOException();
- }
- long transmitted = 0;
- long expected = file.getExpectedSize();
- int count = -1;
- byte[] buffer = new byte[1024];
- while ((count = is.read(buffer)) != -1) {
- transmitted += count;
- os.write(buffer, 0, count);
- updateProgress((int) ((((double) transmitted) / expected) * 100));
+ private void download() throws IOException {
+ InputStream is = null;
+ PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
+ try {
+ wakeLock.acquire();
+ HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
+ if (connection instanceof HttpsURLConnection) {
+ mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ is = new BufferedInputStream(connection.getInputStream());
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ os = AbstractConnectionManager.createOutputStream(file, true);
+ long transmitted = 0;
+ long expected = file.getExpectedSize();
+ int count = -1;
+ byte[] buffer = new byte[1024];
+ while ((count = is.read(buffer)) != -1) {
+ transmitted += count;
+ os.write(buffer, 0, count);
+ updateProgress((int) ((((double) transmitted) / expected) * 100));
+ }
+ os.flush();
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ FileBackend.close(os);
+ FileBackend.close(is);
+ wakeLock.release();
}
- os.flush();
- os.close();
- is.close();
}
private void updateImageBounds() {
@@ -242,10 +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 a3ab8daba2448c5cf0535474eb89760886f4f17e..2e5458423714764cabaf775664bfea69210a420d 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 24e93db16a71fc156426d76723abbb46b2b3699a..18331796e9c6c779faa5592ab3c5b798e3b68193 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 6039d3956e31c41968a5289b5c4446fb7481f52d..e74cb65c0e9b6c0839b80fcbb1851f697706b3ae 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 2d722af7ebd1f7e6eedd6aea3ce970f8ae856fa1..025ed1e7e25568fe2936e448e5b6b3bfff02ee40 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 d11b02fa4a40070b0fcf978b7a4584dab85daefa..9fe47512becd5d72153e2805bbc9de2d11acd8d8 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 ab1912856a121a075d33ae28adbc35c16977ed8b..6e5a1ae355f23c72d898996f91d6338f9e679e85 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -1,5 +1,19 @@
package eu.siacs.conversations.persistance;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@@ -8,6 +22,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.Socket;
import java.net.URL;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
@@ -17,28 +32,15 @@ import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.RectF;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.util.Base64;
-import android.util.Base64OutputStream;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
+import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
@@ -126,25 +128,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;
+ }
+ 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;
}
- return path;
+ }
+
+ public String getOriginalPath(Uri uri) {
+ Log.d(Config.LOGTAG,"get original path for uri: "+uri.toString());
+ return FileUtils.getPath(mXmppConnectionService,uri);
}
public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
@@ -184,8 +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 676a09c97d15d7da4f316eb7aa777f0d8ee6fddf..5def05dd4febd5f40126f57918e138ae9d70c6cb 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 dfbe9db769870c0291533fb5469ace535d11790c..ceab1592b722ca1d1b6e083a5408b1ddfd7d4f6d 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 956f704eb4ecbad238dc13703757377ac2d6f805..90e4d216d5169becc1629282f50597302aeee491 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 f5c54adf5bc428026eb310763908d72e9cfab0bc..a9a2f21172683b3c6cb9a30728bbdea403b2fa09 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 a57e1b89ef46e129750efb43be8a68cc76755552..bd2042fb65e8566cf10aa0e2a5729fa3d7c192aa 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 13d7f4fc84e194b9157168e87ab0fe680934dcac..08594201a7e6c7d1d07fb8de6ecad6d0fe46bd0a 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 54c064c65964a7aba2a954d5fd4dadc62549bdc2..aa986bd1f2cf65b39fa9162be94b7b51557bb44c 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 c9e99ce574066d53bff4c239999331d7bb7d2e79..bc23c3bab392c58e3c1130d238f71455d5c0a2c6 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
@@ -13,11 +13,11 @@ import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.ListView;
-import java.util.Set;
-import java.util.HashSet;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
-import java.util.ArrayList;
+import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -149,4 +149,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
return result.toArray(new String[result.size()]);
}
+
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index 07b8819d963caec4c69af13aaa6e685896f43558..12b66c3509be0d550d55860c6decac0851004b7b 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 c190caede5a66c8e34604821850e63d7914f4024..d98e9164dace0fc8ba586d1c4c84f95440ef5894 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 96abf65b0ffda7dbb12ec3412aa3e2c0bb860b71..219a4fcac2c78f139a3d2e32de5627c65de8afd0 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 d254ece7bc4553eec861e8ced5ac5dd2d0e5d948..de758efc1233424e0e2df771ad54f32f58590059 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 908c29d210b6835ec136cfb8e7453d0addd777ef..02b1d8738b086b78293f2b68ae1a9b51e8c96196 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -1,6 +1,8 @@
package eu.siacs.conversations.ui;
+import android.app.AlertDialog.Builder;
import android.app.PendingIntent;
+import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
@@ -23,18 +25,24 @@ import android.widget.TableLayout;
import android.widget.TextView;
import android.widget.Toast;
+import org.whispersystems.libaxolotl.IdentityKey;
+
+import java.util.Set;
+
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
-public class EditAccountActivity extends XmppActivity implements OnAccountUpdate{
+public class EditAccountActivity extends XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated {
private AutoCompleteTextView mAccountJid;
private EditText mPassword;
@@ -52,14 +60,23 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TextView mServerInfoCSI;
private TextView mServerInfoBlocking;
private TextView mServerInfoPep;
+ private TextView mServerInfoHttpUpload;
private TextView mSessionEst;
private TextView mOtrFingerprint;
+ private TextView mAxolotlFingerprint;
+ private TextView mAccountJidLabel;
private ImageView mAvatar;
private RelativeLayout mOtrFingerprintBox;
+ private RelativeLayout mAxolotlFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton;
+ private ImageButton mAxolotlFingerprintToClipboardButton;
+ private ImageButton mRegenerateAxolotlKeyButton;
+ private LinearLayout keys;
+ private LinearLayout keysCard;
private Jid jidToEdit;
private Account mAccount;
+ private String messageFingerprint;
private boolean mFetchingAvatar = false;
@@ -72,17 +89,34 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
xmppConnectionService.updateAccount(mAccount);
return;
}
- final boolean registerNewAccount = mRegisterNew.isChecked();
+ final boolean registerNewAccount = mRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
+ if (Config.DOMAIN_LOCK != null && mAccountJid.getText().toString().contains("@")) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ mAccountJid.requestFocus();
+ return;
+ }
final Jid jid;
try {
- jid = Jid.fromString(mAccountJid.getText().toString());
+ if (Config.DOMAIN_LOCK != null) {
+ jid = Jid.fromParts(mAccountJid.getText().toString(),Config.DOMAIN_LOCK,null);
+ } else {
+ jid = Jid.fromString(mAccountJid.getText().toString());
+ }
} catch (final InvalidJidException e) {
- mAccountJid.setError(getString(R.string.invalid_jid));
+ if (Config.DOMAIN_LOCK != null) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ } else {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ }
mAccountJid.requestFocus();
return;
}
if (jid.isDomainJid()) {
- mAccountJid.setError(getString(R.string.invalid_jid));
+ if (Config.DOMAIN_LOCK != null) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ } else {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ }
mAccountJid.requestFocus();
return;
}
@@ -108,13 +142,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
xmppConnectionService.updateAccount(mAccount);
} else {
- try {
- if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) {
- mAccountJid.setError(getString(R.string.account_already_exists));
- mAccountJid.requestFocus();
- return;
- }
- } catch (final InvalidJidException e) {
+ if (xmppConnectionService.findAccountByJid(jid) != null) {
+ mAccountJid.setError(getString(R.string.account_already_exists));
+ mAccountJid.requestFocus();
return;
}
mAccount = new Account(jid.toBareJid(), password);
@@ -139,34 +169,33 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
finish();
}
};
- @Override
- public void onAccountUpdate() {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- invalidateOptionsMenu();
- if (mAccount != null
- && mAccount.getStatus() != Account.State.ONLINE
- && mFetchingAvatar) {
- startActivity(new Intent(getApplicationContext(),
- ManageAccountActivity.class));
- finish();
- } else if (jidToEdit == null && mAccount != null
- && mAccount.getStatus() == Account.State.ONLINE) {
- if (!mFetchingAvatar) {
- mFetchingAvatar = true;
- xmppConnectionService.checkForAvatar(mAccount,
- mAvatarFetchCallback);
- }
- } else {
- updateSaveButton();
- }
- if (mAccount != null) {
- updateAccountInformation(false);
- }
+ public void refreshUiReal() {
+ invalidateOptionsMenu();
+ if (mAccount != null
+ && mAccount.getStatus() != Account.State.ONLINE
+ && mFetchingAvatar) {
+ startActivity(new Intent(getApplicationContext(),
+ ManageAccountActivity.class));
+ finish();
+ } else if (jidToEdit == null && mAccount != null
+ && mAccount.getStatus() == Account.State.ONLINE) {
+ if (!mFetchingAvatar) {
+ mFetchingAvatar = true;
+ xmppConnectionService.checkForAvatar(mAccount,
+ mAvatarFetchCallback);
}
- });
+ } else {
+ updateSaveButton();
+ }
+ if (mAccount != null) {
+ updateAccountInformation(false);
+ }
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ refreshUi();
}
private final UiCallback 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 56dbc55e02ecde3084226042b7263adf7ec52bf6..5b9b73550296db5aff610f14fb00a1dbf5f09584 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 4333dbdb0d05e48f022397e85c28d01969ea4357..e01490f9077c4ee6e674ba46a1b8e3bf440ec31b 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 eb5d9b2e0457bdab223d58e2fc5b2c599571f033..6e5fe610bcdfd99869242efe346fdd537e12230f 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 351f1dfc2cc246f9568545250bb3c1b796bf67d0..e0ebd5c2f9fa55347a6b0cb2735858ec9771cac9 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 68e77af4959984a15bf245e197e783fc8ce3555f..74621fc4c9a8aa4f4359cb9e9876f2f550b3f95a 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 0000000000000000000000000000000000000000..cf22416fb1620561f690f5a42e54ed8ef9044e63
--- /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 7c994c31abe1d4f8097ae9797c23b0d1c73edae0..7734dc116d3626e81c2a26265d383091cc40322a 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);
+ }
+
+ public void privateMsgInMuc(Conversation conversation, String nick) {
+ switchToConversation(conversation, null, nick, true, false);
}
- private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) {
+ private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
Intent viewConversationIntent = new Intent(this,
ConversationActivity.class);
viewConversationIntent.setAction(Intent.ACTION_VIEW);
@@ -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 782a1231b6ff8dc2f62d9f40495fbd31515598f3..98250af943339f33bb02e84402eeb35836b0ebd3 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 bfe44326e3443a15afa6eb2873f62d3a9bf0f02b..6918713e406a663688baf60e594f9939e2f16e43 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 0993735f1a7f32530c175e7fdf62c9deb0407940..471526afed36d5ddfa69a8c0cd74e4b2f08dae5a 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 7b20b55fc30fbe5fcd6198e5983405ae40555860..ad7d7622470a335c315d03c18f6637953bf2dbbb 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 167f3f02fae26855feb38a7163506199aa3719ec..1ddd6c44a27f94d0f059b6de24dd1ef29bed4f59 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;
}
- @Override
- public int getItemViewType(int position) {
- if (getItem(position).getType() == Message.TYPE_STATUS) {
+ public int getItemViewType(Message message) {
+ if (message.getType() == Message.TYPE_STATUS) {
return STATUS;
- } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
+ } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
return RECEIVED;
+ }
+
+ return SENT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return this.getItemViewType(getItem(position));
+ }
+
+ private int getMessageTextColor(int type, boolean primary) {
+ if (type == SENT) {
+ return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54);
} else {
- return SENT;
+ return activity.getResources().getColor(primary ? R.color.white : R.color.white70);
}
}
- private void displayStatus(ViewHolder viewHolder, Message message) {
+ private void displayStatus(ViewHolder viewHolder, Message message, int type) {
String filesize = null;
String info = null;
boolean error = false;
@@ -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);
+ }
+ }
+
+ if (type == RECEIVED) {
+ if(message.isValidInSession()) {
+ viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received);
+ } else {
+ viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
}
}
- displayStatus(viewHolder, message);
+ displayStatus(viewHolder, message, type);
return view;
}
@@ -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 0000000000000000000000000000000000000000..fd3b5553fe8d13998411eecaac0b8dbd118c63dc
--- /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 2dec203d948e39e0099bd86456ecd8a90a98dccb..c7c9ac4239b871eff92e682b60c302b124cc18b3 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 9a6897689e7424354ca1a0cca1a129f6eb1846cb..f18a4ed8e67c76451a1432e560c18b1abde1ae33 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 99e8ebb80b9acd3b490bcec4dfd7e18c5bd4be27..b90f06ff9d0b8b2f5c5dfd0e9a1f691eb7d82edc 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 2e768ad91487be0e8538c5f3d75e9a94f2944767..cac23f077b45ccad6e3634f3ba9ed2d897aa3030 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 32657c66c23ecbe5f4012c1c3167a990b5c579ba..dc5a68f6cb5cb5778e2e8cf55136a0000ef4aae7 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 52d3d46acea232ef6f18eb326fc559388f25588e..aeaaa5931ece997d55e6f24161b4b0f5e915bab0 100644
--- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java
+++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java
@@ -1,18 +1,18 @@
package eu.siacs.conversations.xml;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
-import eu.siacs.conversations.Config;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.util.Log;
-import android.util.Xml;
+import eu.siacs.conversations.Config;
public class XmlReader {
private XmlPullParser parser;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
new file mode 100644
index 0000000000000000000000000000000000000000..65ae133d54adc190d94b8f8650e87145da4b0892
--- /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 35c89b45f2bf5cd2f8d2d6727b871e156c8fdf44..5ac089766733f6b2495f8ad798693c0d3e05d3c6 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 65cafe7916a7d1f978c205dc1e92421eb83b6162..7b140842a381213687cb2d9646557c60457ac8c5 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();
+ mXmppConnectionService.getFileBackend().updateFileParams(message);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
if (acceptedAutomatically) {
message.markUnread();
- JingleConnection.this.mXmppConnectionService
- .getNotificationService().push(message);
+ JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
- mXmppConnectionService.getFileBackend().updateFileParams(message);
- mXmppConnectionService.databaseBackend.createMessage(message);
- mXmppConnectionService.markMessage(message,
- Message.STATUS_RECEIVED);
} else {
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
file.delete();
@@ -113,6 +124,14 @@ public class JingleConnection implements Transferable {
}
};
+ public InputStream getFileInputStream() {
+ return this.mFileInputStream;
+ }
+
+ public OutputStream getFileOutputStream() {
+ return this.mFileOutputStream;
+ }
+
private OnProxyActivated onProxyActivated = new OnProxyActivated() {
@Override
@@ -194,7 +213,22 @@ public class JingleConnection implements Transferable {
mXmppConnectionService.sendIqPacket(account,response,null);
}
- public void init(Message message) {
+ public void init(final Message message) {
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ Conversation conversation = message.getConversation();
+ conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() {
+ @Override
+ public void run(XmppAxolotlMessage xmppAxolotlMessage) {
+ init(message, xmppAxolotlMessage);
+ }
+ });
+ } else {
+ init(message, null);
+ }
+ }
+
+ private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
+ this.mXmppAxolotlMessage = xmppAxolotlMessage;
this.contentCreator = "initiator";
this.contentName = this.mJingleConnectionManager.nextRandomId();
this.message = message;
@@ -238,8 +272,7 @@ public class JingleConnection implements Transferable {
});
mergeCandidate(candidate);
} else {
- Log.d(Config.LOGTAG,
- "no primary candidate of our own was found");
+ Log.d(Config.LOGTAG, "no primary candidate of our own was found");
sendInitRequest();
}
}
@@ -267,13 +300,16 @@ public class JingleConnection implements Transferable {
this.contentCreator = content.getAttribute("creator");
this.contentName = content.getAttribute("name");
this.transportId = content.getTransportId();
- this.mergeCandidates(JingleCandidate.parse(content.socks5transport()
- .getChildren()));
+ this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
this.fileOffer = packet.getJingleContent().getFileOffer();
mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
if (fileOffer != null) {
+ Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
+ if (encrypted != null) {
+ this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
+ }
Element fileSize = fileOffer.findChild("size");
Element fileNameElement = fileOffer.findChild("name");
if (fileNameElement != null) {
@@ -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 cadf9df39e4993e2a3d669b11ff43b2967e07727..ab564480c7928a7ed5fdfedb982fa889397aba0c 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 9a02ee7aa96e6ff915f0fb7c5dd2a06ee14772e3..ab7ab73b891100d4e1c73c1b14cf75fb93212d1c 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 8d74f44e332c1fcad9dde199acd66fc1ca852ed2..556395aef34e8ed2c2eb2c5b99c95fc3e6648245 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 e832d3f584044237e937c70016d2e6358c1f843a..b32111584ec03fdfff0674268c40825810a16b39 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 bcadbe77853d62e0196245917b2c81fe7615631c..f752cc5d39e05d0691cee5b52aa9fb57729d1782 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 74da6a9bdbc2f2aa9a4b0e0d5291661be8983259..38bb5c8f9c8b5d420597fc3d8f805eeb8a0e5649 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 7b36fc49736196c40866ec31b9d935b07aa45157..398102e126558d4f8f4bbe92ff351d35c41bb5f0 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 e32811afe39bd04f22b6b7f0cb0f4ae769428bf1..6b69091258763df30e24bf74471d9a6c06d721f1 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 0000000000000000000000000000000000000000..58bf972171f199c359dcdb3bc6233ac84c7853d3
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 0000000000000000000000000000000000000000..d4c06072b56dace7da1ab5fcfc5cb0a494adc5cf
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 0000000000000000000000000000000000000000..51cc4dbd1568c1f132dafa2b73d46c5e66f85bd0
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 2a2934fb1f6ab2f8b92e34c3760c13e58a8fa8a8..220463fc7ac0647354b2bacfa1097db0bd3954d6 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 0000000000000000000000000000000000000000..46eb1195b286a0b51d01382ba7eff4c51a5ce3f8
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 0000000000000000000000000000000000000000..1a496771eb909f3ffdd4937afda2c3baf9d686a7
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 0000000000000000000000000000000000000000..9cd2f825d20537f5df39483f7257c9644f471f27
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 0000000000000000000000000000000000000000..2bb4e04f688d73b1686bc9e743fa5fe1029ac0f5
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 0000000000000000000000000000000000000000..1e9b151b665024f4fc06544e6a40d93e1f5b6f1a
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 0000000000000000000000000000000000000000..b7c1fc11ac5b50084e552596a8c7a6e4c3f64113
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 0000000000000000000000000000000000000000..ca6e490962adf2e24f5e2d9c9b04a093c2747261
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 0000000000000000000000000000000000000000..cbcda5d478f3e326a637242886dee86e76b4649d
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 0000000000000000000000000000000000000000..234b12dc4b93946d4522dca7fb09e8eb600fb7e6
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 0000000000000000000000000000000000000000..cf5fab3adf243318d11ec95448c6deae3f52d9b1
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 0000000000000000000000000000000000000000..5e5e7cf2b14732802095661e2de6a8c6eac3e8b5
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 0000000000000000000000000000000000000000..c136c59fca31bd455695d7dc6ddbc4536a54a7ac
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 5a73aef4b4b14f1c354f2fef34eb35ecb209d8bf..690d4d03d6f23e1dd8cc181726eec6c7700c55ee 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 0000000000000000000000000000000000000000..e2f894ef4436f7a918f952db1db4c2b7059914cd
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 0000000000000000000000000000000000000000..7e134402f159f79924ea82f72da2546d60b1df99
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 0000000000000000000000000000000000000000..38f75e051ad1c435fa0e2e51ae18ffcd422a87aa
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 0000000000000000000000000000000000000000..b42664a85a4fc309e479275ed541ebda24400580
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 0000000000000000000000000000000000000000..968de345d7720405f0c465edac55a45e11167aac
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 0000000000000000000000000000000000000000..51fb4d7a28e49d5cc120a57d05adf757a77a362a
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 0000000000000000000000000000000000000000..ca788445301ed3f69d4fc8a5176f842845a171e7
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 0000000000000000000000000000000000000000..6a93d5f759083e47f0c7fba11c2f87d82174d8e3
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 0000000000000000000000000000000000000000..e8d7bd0f0db67e3074f1b519a3ec5482a70f51a3
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 0000000000000000000000000000000000000000..b8915716e06792d926f397a7b17346f656c86aa1
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 0000000000000000000000000000000000000000..64a4944f7531ab9fb745fd34dd00c778cff1573f
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 0000000000000000000000000000000000000000..7891efffa64deb1140c0c039b32bf4a0493bf96c
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 1f4c9a32e377fc4859013dc6bd8230f720b933c7..cd0d1391a79451135e31157478c79d4ec4adc438 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 0000000000000000000000000000000000000000..b624a8ce6f899b026d106f77bbbe87401a49748c
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 0000000000000000000000000000000000000000..dc8ee5346067fd76101d1e36c42e7e99df4b1de2
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 0000000000000000000000000000000000000000..b5be808f84e5ba67c01ec49102f5ec555a4a1ea3
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 0000000000000000000000000000000000000000..f1c4623268b204e80414a8ce39b72774fabf3ee6
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 0000000000000000000000000000000000000000..7f6773248428bb2bf3d2660202eef8e1318f07d5
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 0000000000000000000000000000000000000000..4199d322b86c67cd1e6a8c1e2adce45d0102c4d7
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 0000000000000000000000000000000000000000..2b86888f300e1150aa12366ef29dd5a757dc4216
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 0000000000000000000000000000000000000000..daa3099009e501f08ce4c35d8be610b14d39348e
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 0000000000000000000000000000000000000000..6aab47c991d275c8e3c116db4ee47b5a7dd11963
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 0000000000000000000000000000000000000000..c9c017410928bb4c9ee56d705130377537886c81
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 0000000000000000000000000000000000000000..9c1e27d74d7b7414a8d4dc18df80ca5da2955ae9
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 1ee9b67dccc335d96dc3a37b7e141d6cbde7f652..6a74ccbefd22f5c4216b52a50e65ea2dfca98a88 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 0000000000000000000000000000000000000000..4945c9595fbde5ad2bb34e0164fd658319350264
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 0000000000000000000000000000000000000000..6ce844871f55002e89a5b76a3d6fc4b186ee6cdf
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 0000000000000000000000000000000000000000..a318186980a2a4f17fa2ec83fe3b5c5b5724e652
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 0000000000000000000000000000000000000000..9f36a6494d311378552cca53a17533db13ac5593
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 0000000000000000000000000000000000000000..db7c1df41490860e6d3840f0084f8b6d618979a3
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 0000000000000000000000000000000000000000..f747b5596a60a401759eb3411e15683eea1993d0
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 0000000000000000000000000000000000000000..b9fe6d469eb5b139988b9074a51bec0d2bb1a85d
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 0000000000000000000000000000000000000000..881990248d52d76dee2b3280dffbc670e80a7cb1
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 0000000000000000000000000000000000000000..7a4fed5405ae19947ab4a73e43d231931f712377
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 0000000000000000000000000000000000000000..2f6d6386de9510fa6dd8c83cbb61a6f2e0fab9b2
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 0000000000000000000000000000000000000000..e44a6d2823f87827cc85b22153932381831463fc
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 0000000000000000000000000000000000000000..e7fa6754bc6123e8e9f627282588ba12aa4216f7
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 0000000000000000000000000000000000000000..398e53a2d589246ea034d7e600da45cfe831bf52
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 0000000000000000000000000000000000000000..be428cd2e0bd2dbd858926f98b5d44bfeb3dcde7
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 0000000000000000000000000000000000000000..3970168ca2dd45cba89a2c295751ba5de593de78
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 0000000000000000000000000000000000000000..ea8d4f8940c3c30d4ddd84b1292c085e0c4bc1e2
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 0000000000000000000000000000000000000000..84d667b131a0af9f19e1ca95ab0e0ae3a3eedede
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 0000000000000000000000000000000000000000..06b190eb930ad601d98ea40fd36bdba9c06b2cc3
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 0000000000000000000000000000000000000000..79c30d1e549127d1848f6ea901a7213506eb8a79
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 0000000000000000000000000000000000000000..20d2fb1460754c9fd8642a43b5e8750573d0f55c
--- /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 0000000000000000000000000000000000000000..45117a98e0d8c8a91b28de3a0bef59fb96654aca
--- /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 0000000000000000000000000000000000000000..ba3d1c45620adbd72b2c0e06246b7f3ef5fa1220
--- /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 06716a10a857d1ab0bcb00a61b2d937ba133441b..cac9a9fa69e861b792651f9d809312ec4e4e7188 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 d7d23f0f91fb9e5c4c6eb88b1058b14d1d1a7355..247e96e59ac8a9b1229fe4387f3423bdf30a2a22 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 98de84f5684a713374583390a7d88a104edd7bb2..0be934a854a4ab5234b0fb8124807c460dad2e95 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 b89c9945b73a78c210b5a9e2cbddce6842ef595e..6edbb706051c38c93391a978809985b6452fd636 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 ab21c693d165f038f786c2edc844f812bdac3bbd..c15f19d5e623a49a85dbe834471a5b18e98d8363 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 933b72b45701c5883dc4ad683ae734bd62e5917d..a4fd29e97de7b779aeb0aad6195f1cebe96b8761 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 28e526e0bd70bd14e26564b81f90c3f26ff494ed..15fa60af4100f991589cfa407532df5c161853ac 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 f8365ad3f2b98cad6a8b42ca8d521b6d71e30cd1..e3b38cd97d153ed938fae6c8dbc6d4ef78c61022 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 2816f475c5d23c22b7562cceb6d41ad2b7076297..ad2579faa9359689288a4579fc54878e628324db 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 adf0ad8dc1ccb83cd267dd886988bae3edd000f1..9af2cd34606069c8104bd39597745bff89b99b8c 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 dc0f5d3eb35a93b187f38e415b6e960ccd2ea9fd..af5e691b7a00da7ff6d0d4bfe4d3d371a30c24bb 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 c5a43674a2fe86d26ae73d812add1df70cec2ac6..0d85e55c5a1a0a4ca3e07b112e8dfb89862d86ff 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 cff228f7a4361aa4aecc625384be15d0369dcce6..17c9061c6b37c54f7fc0b24cac793f1f31f60b47 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 b10fb617ffc343237fa0b342eae66d2d42ad33bd..74d87736bb30353bbfa3b0445f3e337fbac25019 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 190fd575c622f5a09387b73a2997ddfd3c673510..6463d76385189b15b852bffae707c4f75dfa1223 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 efe00a4f56634efdd32ea71759f70747f5feb326..8c77d0a8262e7aeaea7a3ed15ce4926224d715af 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 d796cd6da227d1aa60ef41c239def83193230f19..b9fc352ce5f0e641a7cf286e03eb6f85d5b5f11d 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 29938612f7f52ed1b286bfc1b974e2d2f2990069..f1b5d54c0c0a49894729f7d610af26f03b513ff6 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 028acc532d38df1b1a24d869541f902bf6258e54..0bb59671fd98e502731fdec4e60eade96cfc7359 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 05ac82aced77548d4ef6caa7320bda39f8865898..646f541413d783cfb91c5e1414c17968decbcd46 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 477b758065b4b653e8e37edc78d8c1c88134c211..6a6b23c1def01ac55b83947fcd4b43d491c7db08 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 6f2d967fdb600696a2e74fc5bac34aa7414d5570..f859bde1af2d3d6b50daf64b47606af09b5d6991 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 01299530f2daa41c84d8b7c6b9582641d9e56988..372b87e240bdc74c282e84aeaf8f12286fb0a7d9 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 545ba1c8817ff0bfd06f4ae48aff7815439901fd..8eeaab6b0423bec8edb3c1047fb09083774a7aca 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 353477340dd7cf5c199e6a1a7ff0e67a4cb511d2..35ccc7dd650c0433f3a1472a6b3503a94bbaa83b 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 cdc3737add99287f960f0555aab50456dbd8bc3e..a164dd101802b88540d619294167a8895dec6201 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 80b6934bcf32f8df7927b85d4c7d1f0364e51a50..a4ef81abdbc89c3982363a7700d6c6292ae041db 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 ea2ecc52595650bf1c84232f9cb7591ce92fd6e8..fb94c6051c2b7b6059110c73f8c9f22c1d8bd48e 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 559ae6cfa21710776f0535214b56cc35c296917d..041b5e2dc8811a07f07e17687a0ca39fd1b908ad 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 56a87ffcaca4a1243cc4b1fdde1a884d3b6ced76..053a675c942914129a18746e933c476b5c3d20ce 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 d62387949129a9a0e4e4fde7d486e90ea700b2a1..6f3bf982cfce1f439bdfc5b8baccd47537cc6fff 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 082cfa4651c934cea874a426a8788cc90e755933..96db8b7e38cc5a604813003a6e269c4f6e93ff5d 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 98ab711051cb6b46a11216a274317be75b93cf61..6c4436a8babcb06da4797fdcd0ba1d60e95065d7 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 d1679f928b0fdbf28fd77ea4f36fa6b8eb89cc98..8aa79ac397cd428ba6c6155d750ee792a428380b 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 0d95ee180ddda0b3322898144dfb3c9ffa110ee6..7a67ffdd06ae1892e4adfc2c5dd009013b33030e 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 1fd7c4bf2f5ab00d16aa907bdcabd29e7011c28e..8a4d3539e82bc8a393d67485811008dd10c87cbc 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 e314f752f1813487b1c6b01d409c4dcdd7a2a13c..d471e54ac812b9f83d024f226986a24d5d26833b 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 3a778a210c14444630976753a7f2d8cc45d5e21e..c2a3ad60c88f2e155fa0b0753182809d8b897358 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 dc29ffd331f3201812a844604db24013900bcb52..0e396491d6e2121c0ec2ca8e38d8627abc4b4115 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 b98a37fcb3dbfe2ff866103c3619d0b34b39cc53..e8572d9d46899156151733bd66ded20569d53482 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 5c67203be86d0e342b173db7e0db1a24b41d9dc4..afdc3e809ff2ace871aa0b1d65589e07b5e25059 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 5b7d69040a294b4e9bd081be58822e183a0591bf..8ab35c09886a9beb9f26182f58fde679e4a0b7d9 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"/>
-
+