diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e9d087c..98f7cb83d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.6.0 +* Introduce expert setting to perform channel discovery on local server instead of [search.jabber.network](https://search.jabber.network) +* Enable delivery check marks by default and remove setting +* Enable ‘Send button indicates status’ by default and remove setting +* Move Backup and Foreground Service settings to main screen + ### Version 2.5.12 * Jingle file transfer fixes * Fixed OMEMO self healing (after backup restore) on servers w/o MAM diff --git a/README.md b/README.md index 7a25b3fe1..93d95b1ac 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,13 @@ build your apk file. XMPP, like email, is a federated protocol, which means that there is not one company you can create an *official XMPP account* with. Instead there are hundreds, or even thousands, of providers out there. One of those providers is our very own [chat.sum7.eu](https://chat.sum7.eu). If you don’t like to use *chat.sum7.eu* use a web search engine of your choice to find another provider. Or maybe your university has one. Or you can run your own. Or ask a friend to run one. Once you've found one, you can use Conversations to create an account. Just select *register new account* on server within the create account dialog. ##### Running your own -If you already have a server somewhere and are willing and able to put the necessary work in, one alternative-in the spirit of federation-is to run your own. We recommend either [Prosody](https://prosody.im/) or [ejabberd](https://www.ejabberd.im/). Both of which have their own strengths. Ejabberd is slightly more mature nowadays but Prosody is arguably easier to set up. +If you already have a server somewhere and are willing and able to put the necessary work in you can run your own XMPP server. -For Prosody you need a couple of so called [community modules](https://modules.prosody.im/) most of which are maintained by the same people that develop Prosody. +As of 2019 we recommend you use [ejabberd](https://ejabberd.im). The default configuration file already enables everything you need to pass the [Conversations Compliance Suite](https://compliance.conversations.im). Make sure your Linux distribution ships a fairly recent version. -If you pick ejabberd make sure you use the latest version. Linux Distributions might bundle some very old versions of it. +With a little bit of effort [Prosody](https://prosody.im) can be configured to support all necessary extensions as well. However you will have to rely on so called [Community Modules](https://modules.prosody.im/) of varying quality. Prosody can be interesting to people who like to modify their server and create / prototype own modules. + +Performance wise - for small deployments - both ejabberd and Prosody should be fine. #### Where can I set up a custom hostname / port Conversations will automatically look up the SRV records for your domain name diff --git a/build.gradle b/build.gradle index da8d493bb..447d8aa6b 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ dependencies { implementation "com.leinardi.android:speed-dial:2.0.1" implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:converter-gson:2.6.1' - implementation 'com.squareup.okhttp3:okhttp:3.12.5' + implementation 'com.squareup.okhttp3:okhttp:3.12.6' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.16' } @@ -84,8 +84,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 346 - versionName "2.5.12" + versionCode 347 + versionName "2.6.0-beta" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 36cd26425..b7af9659b 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -90,7 +90,7 @@ public final class Config { public static final int REFRESH_UI_INTERVAL = 500; public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096; - public static final int MAX_STORAGE_MESSAGE_CHARS = 1024 * 1024 * 1024; + public static final int MAX_STORAGE_MESSAGE_CHARS = 1024 * 1024; //1MB public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 2989f356a..29d286a71 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -201,6 +201,9 @@ public class PgpDecryptionService { if (fixedFile.getParentFile().mkdirs()) { Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath()); } + synchronized (mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION) { + mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION.add(outputFile.getAbsolutePath()); + } if (outputFile.renameTo(fixedFile)) { Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath()); message.setRelativeFilePath(path); diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 81463d9e9..116906fef 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -335,7 +335,7 @@ public class MucOptions { } public boolean isContactInRoom(Contact contact) { - return findUserByRealJid(contact.getJid().asBareJid()) != null; + return contact != null && findUserByRealJid(contact.getJid().asBareJid()) != null; } public boolean isUserInRoom(Jid jid) { diff --git a/src/main/java/eu/siacs/conversations/entities/Room.java b/src/main/java/eu/siacs/conversations/entities/Room.java new file mode 100644 index 000000000..74ce07c91 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Room.java @@ -0,0 +1,90 @@ +package eu.siacs.conversations.entities; + +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.common.collect.ComparisonChain; + +import java.util.Comparator; + +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.utils.LanguageUtils; +import eu.siacs.conversations.utils.UIHelper; +import rocks.xmpp.addr.Jid; + +public class Room implements AvatarService.Avatarable, Comparable { + + public String address; + public String name; + public String description; + public String language; + public int nusers; + + public Room(String address, String name, String description, String language, int nusers) { + this.address = address; + this.name = name; + this.description = description; + this.language = language; + this.nusers = nusers; + } + + public Room() { + + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Jid getRoom() { + try { + return Jid.of(address); + } catch (IllegalArgumentException e) { + return null; + } + } + + public String getLanguage() { + return LanguageUtils.convert(language); + } + + @Override + public int getAvatarBackgroundColor() { + Jid room = getRoom(); + return UIHelper.getColorForName(room != null ? room.asBareJid().toEscapedString() : name); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Room room = (Room) o; + return Objects.equal(address, room.address) && + Objects.equal(name, room.name) && + Objects.equal(description, room.description); + } + + @Override + public int hashCode() { + return Objects.hashCode(address, name, description); + } + + + public boolean contains(String needle) { + return Strings.nullToEmpty(name).contains(needle) + || Strings.nullToEmpty(description).contains(needle) + || Strings.nullToEmpty(address).contains(needle); + } + + @Override + public int compareTo(Room o) { + return ComparisonChain.start() + .compare(o.nusers, nusers) + .compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name)) + .compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address)) + .result(); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 7c7456776..1f9de7e6c 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -565,4 +565,18 @@ public class IqGenerator extends AbstractGenerator { } return packet; } + + public IqPacket queryDiscoItems(Jid jid) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(jid); + packet.addChild("query",Namespace.DISCO_ITEMS); + return packet; + } + + public IqPacket queryDiscoInfo(Jid jid) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(jid); + packet.addChild("query",Namespace.DISCO_INFO); + 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 dbf8e7cb2..56445e0d6 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -37,16 +37,14 @@ public class MessageGenerator extends AbstractGenerator { if (conversation.getMode() == Conversation.MODE_SINGLE) { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); - if (this.mXmppConnectionService.indicateReceived() && !isWithSelf) { + if (!isWithSelf) { packet.addChild("request", "urn:xmpp:receipts"); } } else if (message.isPrivateMessage()) { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); packet.addChild("x", "http://jabber.org/protocol/muc#user"); - if (this.mXmppConnectionService.indicateReceived()) { - packet.addChild("request", "urn:xmpp:receipts"); - } + packet.addChild("request", "urn:xmpp:receipts"); } else { packet.setTo(message.getCounterpart().asBareJid()); packet.setType(MessagePacket.TYPE_GROUPCHAT); diff --git a/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java b/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java index 89a8e0ec4..9fae92319 100644 --- a/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java +++ b/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java @@ -1,20 +1,15 @@ package eu.siacs.conversations.http.services; -import com.google.common.base.Objects; - import java.util.Collections; import java.util.List; import java.util.Set; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.utils.LanguageUtils; -import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.entities.Room; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Query; -import rocks.xmpp.addr.Jid; public interface MuclumbusService { @@ -31,55 +26,6 @@ public interface MuclumbusService { public List items; } - class Room implements AvatarService.Avatarable { - - public String address; - public String name; - public String description; - public String language; - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public Jid getRoom() { - try { - return Jid.of(address); - } catch (IllegalArgumentException e) { - return null; - } - } - - public String getLanguage() { - return LanguageUtils.convert(language); - } - - @Override - public int getAvatarBackgroundColor() { - Jid room = getRoom(); - return UIHelper.getColorForName(room != null ? room.asBareJid().toEscapedString() : name); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Room room = (Room) o; - return Objects.equal(address, room.address) && - Objects.equal(name, room.name) && - Objects.equal(description, room.description); - } - - @Override - public int hashCode() { - return Objects.hashCode(address, name, description); - } - } - class SearchRequest { public final Set keywords; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 3042e510f..e5ef662bb 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.parser; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.util.Pair; @@ -27,12 +28,15 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Room; +import eu.siacs.conversations.services.ChannelDiscoveryService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; @@ -417,4 +421,55 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } + + public static List items(IqPacket packet) { + ArrayList items = new ArrayList<>(); + final Element query = packet.findChild("query", Namespace.DISCO_ITEMS); + if (query == null) { + return items; + } + for(Element child : query.getChildren()) { + if ("item".equals(child.getName())) { + Jid jid = child.getAttributeAsJid("jid"); + if (jid != null) { + items.add(jid); + } + } + } + return items; + } + + public static Room parseRoom(IqPacket packet) { + final Element query = packet.findChild("query", Namespace.DISCO_INFO); + if(query == null) { + return null; + } + final Element x = query.findChild("x"); + if (x == null) { + return null; + } + final Element identity = query.findChild("identity"); + Data data = Data.parse(x); + String address = packet.getFrom().toEscapedString(); + String name = identity == null ? null : identity.getAttribute("name"); + String roomName = data.getValue("muc#roomconfig_roomname");; + String description = data.getValue("muc#roominfo_description"); + String language = data.getValue("muc#roominfo_lang"); + String occupants = data.getValue("muc#roominfo_occupants"); + int nusers; + try { + nusers = occupants == null ? 0 : Integer.parseInt(occupants); + } catch (NumberFormatException e) { + nusers = 0; + } + + return new Room( + address, + TextUtils.isEmpty(roomName) ? name : roomName, + description, + language, + nusers + ); + } + } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index b9bedec1a..84bee6408 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -827,6 +827,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender")); if (packet.fromAccount(account) && !selfAddressed) { dismissNotification(account, counterpart, query); + if (query == null) { + activateGracePeriod(account); + } } else if (isTypeGroupChat) { Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); if (conversation != null && id != null && sender != null) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 37168016a..3c049bf48 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -356,7 +356,8 @@ public class FileBackend { if (stream != null) { try { stream.close(); - } catch (IOException e) { + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to close stream", e); } } } @@ -366,6 +367,7 @@ public class FileBackend { try { socket.close(); } catch (IOException e) { + Log.d(Config.LOGTAG, "unable to close socket", e); } } } @@ -375,6 +377,7 @@ public class FileBackend { try { socket.close(); } catch (IOException e) { + Log.d(Config.LOGTAG, "unable to close server socket", e); } } } diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 0e349e508..ca2d534b2 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -38,6 +38,7 @@ import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.RawBlockable; +import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.http.services.MuclumbusService; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; @@ -81,14 +82,14 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { return get((ListItem) avatarable, size, cachedOnly); } else if (avatarable instanceof MucOptions.User) { return get((MucOptions.User) avatarable, size, cachedOnly); - } else if (avatarable instanceof MuclumbusService.Room) { - return get((MuclumbusService.Room) avatarable, size, cachedOnly); + } else if (avatarable instanceof Room) { + return get((Room) avatarable, size, cachedOnly); } throw new AssertionError("AvatarService does not know how to generate avatar from "+avatarable.getClass().getName()); } - private Bitmap get(final MuclumbusService.Room result, final int size, boolean cacheOnly) { + private Bitmap get(final Room result, final int size, boolean cacheOnly) { final Jid room = result.getRoom(); Conversation conversation = room != null ? mXmppConnectionService.findFirstMuc(room) : null; if (conversation != null) { diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index f5dd08485..f01bd72cc 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -7,14 +7,27 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.utils.LanguageUtils; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Call; @@ -22,6 +35,7 @@ import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; +import rocks.xmpp.addr.Jid; public class ChannelDiscoveryService { @@ -30,7 +44,7 @@ public class ChannelDiscoveryService { private MuclumbusService muclumbusService; - private final Cache> cache; + private final Cache> cache; ChannelDiscoveryService(XmppConnectionService service) { this.service = service; @@ -56,21 +70,28 @@ public class ChannelDiscoveryService { this.muclumbusService = retrofit.create(MuclumbusService.class); } - void discover(String query, OnChannelSearchResultsFound onChannelSearchResultsFound) { - final boolean all = query == null || query.trim().isEmpty(); - List result = cache.getIfPresent(all ? "" : query); + void cleanCache() { + cache.invalidateAll(); + } + + void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) { + List result = cache.getIfPresent(key(method, query)); if (result != null) { onChannelSearchResultsFound.onChannelSearchResultsFound(result); return; } - if (all) { - discoverChannels(onChannelSearchResultsFound); + if (method == Method.LOCAL_SERVER) { + discoverChannelsLocalServers(query, onChannelSearchResultsFound); } else { - discoverChannels(query, onChannelSearchResultsFound); + if (query.isEmpty()) { + discoverChannelsJabberNetwork(onChannelSearchResultsFound); + } else { + discoverChannelsJabberNetwork(query, onChannelSearchResultsFound); + } } } - private void discoverChannels(OnChannelSearchResultsFound listener) { + private void discoverChannelsJabberNetwork(OnChannelSearchResultsFound listener) { Call call = muclumbusService.getRooms(1); try { call.enqueue(new Callback() { @@ -82,7 +103,7 @@ public class ChannelDiscoveryService { logError(response); return; } - cache.put("", body.items); + cache.put(key(Method.JABBER_NETWORK, ""), body.items); listener.onChannelSearchResultsFound(body.items); } @@ -97,7 +118,7 @@ public class ChannelDiscoveryService { } } - private void discoverChannels(final String query, OnChannelSearchResultsFound listener) { + private void discoverChannelsJabberNetwork(final String query, OnChannelSearchResultsFound listener) { MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query); Call searchResultCall = muclumbusService.search(searchRequest); @@ -110,7 +131,7 @@ public class ChannelDiscoveryService { logError(response); return; } - cache.put(query, body.result.items); + cache.put(key(Method.JABBER_NETWORK, query), body.result.items); listener.onChannelSearchResultsFound(body.result.items); } @@ -122,6 +143,102 @@ public class ChannelDiscoveryService { }); } + private void discoverChannelsLocalServers(final String query, final OnChannelSearchResultsFound listener) { + final Map localMucService = getLocalMucServices(); + Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); + if (localMucService.size() == 0) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + return; + } + if (!query.isEmpty()) { + final List cached = cache.getIfPresent(key(Method.LOCAL_SERVER, "")); + if (cached != null) { + final List results = copyMatching(cached, query); + cache.put(key(Method.LOCAL_SERVER, query), results); + listener.onChannelSearchResultsFound(results); + } + } + final AtomicInteger queriesInFlight = new AtomicInteger(); + final List rooms = new ArrayList<>(); + for (Map.Entry entry : localMucService.entrySet()) { + IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); + queriesInFlight.incrementAndGet(); + service.sendIqPacket(entry.getValue(), itemsRequest, (account, itemsResponse) -> { + if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { + final List items = IqParser.items(itemsResponse); + for (Jid item : items) { + IqPacket infoRequest = service.getIqGenerator().queryDiscoInfo(item); + queriesInFlight.incrementAndGet(); + service.sendIqPacket(account, infoRequest, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket infoResponse) { + if (infoResponse.getType() == IqPacket.TYPE.RESULT) { + final Room room = IqParser.parseRoom(infoResponse); + if (room != null) { + rooms.add(room); + } + if (queriesInFlight.decrementAndGet() <= 0) { + finishDiscoSearch(rooms, query, listener); + } + } else { + queriesInFlight.decrementAndGet(); + } + } + }); + } + } + if (queriesInFlight.decrementAndGet() <= 0) { + finishDiscoSearch(rooms, query, listener); + } + }); + } + } + + private void finishDiscoSearch(List rooms, String query, OnChannelSearchResultsFound listener) { + Collections.sort(rooms); + cache.put(key(Method.LOCAL_SERVER, ""), rooms); + if (query.isEmpty()) { + listener.onChannelSearchResultsFound(rooms); + } else { + List results = copyMatching(rooms, query); + cache.put(key(Method.LOCAL_SERVER, query), results); + listener.onChannelSearchResultsFound(rooms); + } + } + + private static List copyMatching(List haystack, String needle) { + ArrayList result = new ArrayList<>(); + for (Room room : haystack) { + if (room.contains(needle)) { + result.add(room); + } + } + return result; + } + + private Map getLocalMucServices() { + final HashMap localMucServices = new HashMap<>(); + for (Account account : service.getAccounts()) { + if (account.isEnabled()) { + final XmppConnection xmppConnection = account.getXmppConnection(); + if (xmppConnection == null) { + continue; + } + for (final String mucService : xmppConnection.getMucServers()) { + Jid jid = Jid.of(mucService); + if (!localMucServices.containsKey(jid)) { + localMucServices.put(jid, account); + } + } + } + } + return localMucServices; + } + + private static String key(Method method, String query) { + return String.format("%s\00%s", method, query); + } + private static void logError(final Response response) { final ResponseBody errorBody = response.errorBody(); Log.d(Config.LOGTAG, "code from muclumbus=" + response.code()); @@ -129,13 +246,18 @@ public class ChannelDiscoveryService { return; } try { - Log.d(Config.LOGTAG,"error body="+errorBody.string()); + Log.d(Config.LOGTAG, "error body=" + errorBody.string()); } catch (IOException e) { //ignored } } public interface OnChannelSearchResultsFound { - void onChannelSearchResultsFound(List results); + void onChannelSearchResultsFound(List results); + } + + public enum Method { + JABBER_NETWORK, + LOCAL_SERVER } } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index f35c83f24..a5ed8c67b 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -442,6 +442,8 @@ public class NotificationService { } catch (SecurityException e) { Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString()); } + } else { + mBuilder.setLocalOnly(true); } if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mBuilder.setCategory(Notification.CATEGORY_MESSAGE); @@ -563,14 +565,16 @@ public class NotificationService { if (addedActionsCount < 3) { final Message firstLocationMessage = getFirstLocationMessage(messages); if (firstLocationMessage != null) { - String label = mXmppConnectionService.getResources().getString(R.string.show_location); - PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage); - NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder( - R.drawable.ic_room_white_24dp, - label, - pendingShowLocationIntent).build(); - mBuilder.addAction(locationAction); - ++addedActionsCount; + final PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage); + if (pendingShowLocationIntent != null) { + final String label = mXmppConnectionService.getResources().getString(R.string.show_location); + NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder( + R.drawable.ic_room_white_24dp, + label, + pendingShowLocationIntent).build(); + mBuilder.addAction(locationAction); + ++addedActionsCount; + } } } if (addedActionsCount < 3) { @@ -766,7 +770,7 @@ public class NotificationService { return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT); } } - return createOpenConversationsIntent(); + return null; } private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { @@ -906,7 +910,10 @@ public class NotificationService { } } mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); - mBuilder.setContentIntent(createOpenConversationsIntent()); + final PendingIntent openIntent = createOpenConversationsIntent(); + if (openIntent != null) { + mBuilder.setContentIntent(openIntent); + } mBuilder.setWhen(0); mBuilder.setPriority(Notification.PRIORITY_MIN); mBuilder.setSmallIcon(connected > 0 ? R.drawable.ic_link_white_24dp : R.drawable.ic_link_off_white_24dp); @@ -920,7 +927,11 @@ public class NotificationService { } private PendingIntent createOpenConversationsIntent() { - return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0); + try { + return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0); + } catch (RuntimeException e) { + return null; + } } void updateErrorNotification() { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 5eef556e2..df2aaec19 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -34,6 +34,7 @@ import android.provider.ContactsContract; import android.security.KeyChain; import android.support.annotation.BoolRes; import android.support.annotation.IntegerRes; +import android.support.annotation.NonNull; import android.support.v4.app.RemoteInput; import android.support.v4.content.ContextCompat; import android.text.TextUtils; @@ -42,6 +43,8 @@ import android.util.Log; import android.util.LruCache; import android.util.Pair; +import com.google.common.base.Strings; + import org.conscrypt.Conscrypt; import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.util.OpenPgpApi; @@ -277,6 +280,9 @@ public class XmppConnectionService extends Service { private final Object LISTENER_LOCK = new Object(); + public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); + + private final OnBindListener mOnBindListener = new OnBindListener() { @Override @@ -854,8 +860,8 @@ public class XmppConnectionService extends Service { mChannelDiscoveryService.initializeMuclumbusService(); } - public void discoverChannels(String query, ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) { - mChannelDiscoveryService.discover(query, onChannelSearchResultsFound); + public void discoverChannels(String query, ChannelDiscoveryService.Method method, ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) { + mChannelDiscoveryService.discover(Strings.nullToEmpty(query).trim(), method, onChannelSearchResultsFound); } public boolean isDataSaverDisabled() { @@ -1831,6 +1837,12 @@ public class XmppConnectionService extends Service { } private void markFileDeleted(final String path) { + synchronized (FILENAMES_TO_IGNORE_DELETION) { + if (FILENAMES_TO_IGNORE_DELETION.remove(path)) { + Log.d(Config.LOGTAG,"ignored deletion of "+path); + return; + } + } final File file = new File(path); final boolean isInternalFile = fileBackend.isInternalFile(file); final List uuids = databaseBackend.markFileAsDeleted(file, isInternalFile); @@ -1951,9 +1963,13 @@ public class XmppConnectionService extends Service { * This will find all conferences with the contact as member and also the conference that is the contact (that 'fake' contact is used to store the avatar) */ public List findAllConferencesWith(Contact contact) { - ArrayList results = new ArrayList<>(); + final ArrayList results = new ArrayList<>(); for (final Conversation c : conversations) { - if (c.getMode() == Conversation.MODE_MULTI && (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || c.getMucOptions().isContactInRoom(contact))) { + if (c.getMode() != Conversation.MODE_MULTI) { + continue; + } + final MucOptions mucOptions = c.getMucOptions(); + if (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || (mucOptions != null && mucOptions.isContactInRoom(contact))) { results.add(c); } } @@ -2231,6 +2247,7 @@ public class XmppConnectionService extends Service { getNotificationService().updateErrorNotification(); toggleForegroundService(); syncEnabledAccountSetting(); + mChannelDiscoveryService.cleanCache(); return true; } else { return false; @@ -3073,9 +3090,7 @@ public class XmppConnectionService extends Service { } public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) { - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(conversation.getJid().asBareJid()); - request.query("http://jabber.org/protocol/disco#info"); + IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -3891,10 +3906,6 @@ public class XmppConnectionService extends Service { return getBooleanPreference("autojoin", R.bool.autojoin); } - public boolean indicateReceived() { - return getBooleanPreference("indicate_received", R.bool.indicate_received); - } - public boolean useTorToConnect() { return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); } @@ -4424,7 +4435,7 @@ public class XmppConnectionService extends Service { request.setTo(jid); final String node = presence.getNode(); final String ver = presence.getVer(); - final Element query = request.query("http://jabber.org/protocol/disco#info"); + final Element query = request.query(Namespace.DISCO_INFO); if (node != null && ver != null) { query.setAttribute("node", node + "#" + ver); } diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index b1684473d..5dfcf9bdc 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -7,8 +7,10 @@ import android.content.SharedPreferences; import android.databinding.DataBindingUtil; import android.net.Uri; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.v7.widget.Toolbar; import android.text.Html; +import android.text.method.LinkMovementMethod; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -27,11 +29,12 @@ import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.http.services.MuclumbusService; +import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.services.ChannelDiscoveryService; import eu.siacs.conversations.ui.adapter.ChannelSearchResultAdapter; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; +import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.AccountUtils; import rocks.xmpp.addr.Jid; @@ -45,6 +48,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private MenuItem mMenuSearchView; private EditText mSearchEditText; + private ChannelDiscoveryService.Method method = ChannelDiscoveryService.Method.LOCAL_SERVER; + private boolean optedIn = false; @Override @@ -54,14 +59,15 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O @Override void onBackendConnected() { - if (optedIn) { - String query; + if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { + final String query; if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { query = mSearchEditText.getText().toString(); } else { query = mInitialSearchValue.peek(); } - xmppConnectionService.discoverChannels(query, this); + toggleLoadingScreen(); + xmppConnectionService.discoverChannels(query, this.method, this); } } @@ -73,29 +79,39 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O configureActionBar(getSupportActionBar(), true); binding.list.setAdapter(this.adapter); this.adapter.setOnChannelSearchResultSelectedListener(this); - optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false); + this.optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false); final String search = savedInstanceState == null ? null : savedInstanceState.getString("search"); if (search != null) { mInitialSearchValue.push(search); } + } + private static ChannelDiscoveryService.Method getMethod(final Context c) { + final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(c); + final String m = p.getString("channel_discovery_method", c.getString(R.string.default_channel_discovery)); + try { + return ChannelDiscoveryService.Method.valueOf(m); + } catch (IllegalArgumentException e) { + return ChannelDiscoveryService.Method.JABBER_NETWORK; + } } @Override public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.muc_users_activity, menu); + getMenuInflater().inflate(R.menu.channel_discovery_activity, menu); + AccountUtils.showHideMenuItems(menu); mMenuSearchView = menu.findItem(R.id.action_search); final View mSearchView = mMenuSearchView.getActionView(); mSearchEditText = mSearchView.findViewById(R.id.search_field); mSearchEditText.setHint(R.string.search_channels); - String initialSearchValue = mInitialSearchValue.pop(); + final String initialSearchValue = mInitialSearchValue.pop(); if (initialSearchValue != null) { mMenuSearchView.expandActionView(); mSearchEditText.append(initialSearchValue); mSearchEditText.requestFocus(); - if (optedIn && xmppConnectionService != null) { - xmppConnectionService.discoverChannels(initialSearchValue, this); + if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) && xmppConnectionService != null) { + xmppConnectionService.discoverChannels(initialSearchValue, this.method, this); } } mSearchEditText.setOnEditorActionListener(this); @@ -119,8 +135,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); mSearchEditText.setText(""); toggleLoadingScreen(); - if (optedIn) { - xmppConnectionService.discoverChannels(null, this); + if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { + xmppConnectionService.discoverChannels(null, this.method, this); } return true; } @@ -128,12 +144,14 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private void toggleLoadingScreen() { adapter.submitList(Collections.emptyList()); binding.progressBar.setVisibility(View.VISIBLE); + binding.list.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary)); } @Override public void onStart() { super.onStart(); - if (!optedIn) { + this.method = getMethod(this); + if (!optedIn && method == ChannelDiscoveryService.Method.JABBER_NETWORK) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.channel_discovery_opt_in_title); builder.setMessage(Html.fromHtml(getString(R.string.channel_discover_opt_in_message))); @@ -141,11 +159,25 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn()); builder.setOnCancelListener(dialog -> finish()); final AlertDialog dialog = builder.create(); + dialog.setOnShowListener(d -> { + final TextView textView = dialog.findViewById(android.R.id.message); + if (textView == null) { + return; + } + textView.setMovementMethod(LinkMovementMethod.getInstance()); + }); dialog.setCanceledOnTouchOutside(false); dialog.show(); + holdLoading(); } } + private void holdLoading() { + adapter.submitList(Collections.emptyList()); + binding.progressBar.setVisibility(View.GONE); + binding.list.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary)); + } + @Override public void onSaveInstanceState(Bundle savedInstanceState) { if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { @@ -158,31 +190,36 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O SharedPreferences preferences = getPreferences(); preferences.edit().putBoolean(CHANNEL_DISCOVERY_OPT_IN, true).apply(); optedIn = true; - xmppConnectionService.discoverChannels(null, this); + toggleLoadingScreen(); + xmppConnectionService.discoverChannels(null, this.method, this); } @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (optedIn) { - xmppConnectionService.discoverChannels(v.getText().toString(), this); + if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { + toggleLoadingScreen(); + SoftKeyboardUtils.hideSoftKeyboard(this); + xmppConnectionService.discoverChannels(v.getText().toString(), this.method, this); } - toggleLoadingScreen(); - SoftKeyboardUtils.hideSoftKeyboard(this); return true; } @Override - public void onChannelSearchResultsFound(List results) { + public void onChannelSearchResultsFound(final List results) { runOnUiThread(() -> { adapter.submitList(results); - binding.list.setVisibility(View.VISIBLE); binding.progressBar.setVisibility(View.GONE); + if (results.size() == 0) { + binding.list.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_primary_background_no_results)); + } else { + binding.list.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary)); + } }); } @Override - public void onChannelSearchResult(final MuclumbusService.Room result) { + public void onChannelSearchResult(final Room result) { List accounts = AccountUtils.getEnabledAccounts(xmppConnectionService); if (accounts.size() == 1) { joinChannelSearchResult(accounts.get(0), result); @@ -200,7 +237,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O @Override public boolean onContextItemSelected(MenuItem item) { - final MuclumbusService.Room room = adapter.getCurrent(); + final Room room = adapter.getCurrent(); if (room != null) { switch (item.getItemId()) { case R.id.share_with: @@ -218,7 +255,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O return false; } - public void joinChannelSearchResult(String selectedAccount, MuclumbusService.Room result) { + public void joinChannelSearchResult(String selectedAccount, Room result) { final Jid jid = Config.DOMAIN_LOCK == null ? Jid.of(selectedAccount) : Jid.of(selectedAccount, Config.DOMAIN_LOCK, null); final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin); final Account account = xmppConnectionService.findAccountByJid(jid); diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java index cfb4f05fb..fca082c42 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java @@ -5,6 +5,7 @@ import android.net.Uri; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.widget.ListView; +import android.widget.Toast; import java.util.ArrayList; import java.util.List; @@ -78,7 +79,12 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity { intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString()); intent.setData(uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(intent); + try { + startActivity(intent); + } catch (SecurityException e) { + Toast.makeText(this, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show(); + return; + } } finish(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index c0ec6d790..1eaa1861f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2267,7 +2267,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public void updateSendButton() { boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); - boolean useSendButtonToIndicateStatus = activity != null && PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("send_button_status", getResources().getBoolean(R.bool.send_button_status)); final Conversation c = this.conversation; final Presence.Status status; final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); @@ -2277,7 +2276,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else { action = SendButtonTool.getAction(getActivity(), c, text); } - if (useSendButtonToIndicateStatus && c.getAccount().getStatus() == Account.State.ONLINE) { + if (c.getAccount().getStatus() == Account.State.ONLINE) { if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { status = Presence.Status.OFFLINE; } else if (c.getMode() == Conversation.MODE_SINGLE) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index d8c208dd4..1a24077ad 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -228,20 +228,22 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat removeErrorsOnAllBut(binding.hostnameLayout); return; } - try { - numericPort = Integer.parseInt(port); - if (numericPort < 0 || numericPort > 65535) { + if (!hostname.isEmpty()) { + try { + numericPort = Integer.parseInt(port); + if (numericPort < 0 || numericPort > 65535) { + binding.portLayout.setError(getString(R.string.not_a_valid_port)); + removeErrorsOnAllBut(binding.portLayout); + binding.port.requestFocus(); + return; + } + + } catch (NumberFormatException e) { binding.portLayout.setError(getString(R.string.not_a_valid_port)); removeErrorsOnAllBut(binding.portLayout); binding.port.requestFocus(); return; } - - } catch (NumberFormatException e) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); - return; } } @@ -477,8 +479,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void updatePortLayout() { - String hostname = this.binding.hostname.getText().toString(); - this.binding.portLayout.setEnabled(!TextUtils.isEmpty(hostname)); + final String hostname = this.binding.hostname.getText().toString(); + if (TextUtils.isEmpty(hostname)) { + this.binding.portLayout.setEnabled(false); + this.binding.portLayout.setError(null); + } else { + this.binding.portLayout.setEnabled(true); + } } protected void updateSaveButton() { @@ -613,7 +620,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void refreshAvatar() { - AvatarWorkerTask.loadAvatar(mAccount,binding.avater,R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); } @Override @@ -683,9 +690,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } boolean init = intent.getBooleanExtra("init", false); boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false); - Log.d(Config.LOGTAG,"extras "+intent.getExtras()); - this.mForceRegister = intent.hasExtra(EXTRA_FORCE_REGISTER) ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER,false) : null; - Log.d(Config.LOGTAG,"force register="+mForceRegister); + Log.d(Config.LOGTAG, "extras " + intent.getExtras()); + this.mForceRegister = intent.hasExtra(EXTRA_FORCE_REGISTER) ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) : null; + Log.d(Config.LOGTAG, "force register=" + mForceRegister); this.mInitMode = init || this.jidToEdit == null; this.messageFingerprint = intent.getStringExtra("fingerprint"); if (!mInitMode) { @@ -975,7 +982,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (!mInitMode) { this.binding.avater.setVisibility(View.VISIBLE); - AvatarWorkerTask.loadAvatar(mAccount,binding.avater,R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); } else { this.binding.avater.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 0ec7abd9a..dd4ea40a2 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -200,7 +200,12 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer intent.putExtra(Intent.EXTRA_TEXT, share.text); intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, share.asQuote); } - startActivity(intent); + try { + startActivity(intent); + } catch (SecurityException e) { + Toast.makeText(this, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show(); + return; + } finish(); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java index 5ba28c446..0f452e0b1 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java @@ -16,26 +16,26 @@ import java.util.Locale; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.SearchResultItemBinding; -import eu.siacs.conversations.http.services.MuclumbusService; +import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import rocks.xmpp.addr.Jid; -public class ChannelSearchResultAdapter extends ListAdapter implements View.OnCreateContextMenuListener { +public class ChannelSearchResultAdapter extends ListAdapter implements View.OnCreateContextMenuListener { - private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { + private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { @Override - public boolean areItemsTheSame(@NonNull MuclumbusService.Room a, @NonNull MuclumbusService.Room b) { + public boolean areItemsTheSame(@NonNull Room a, @NonNull Room b) { return a.address != null && a.address.equals(b.address); } @Override - public boolean areContentsTheSame(@NonNull MuclumbusService.Room a, @NonNull MuclumbusService.Room b) { + public boolean areContentsTheSame(@NonNull Room a, @NonNull Room b) { return a.equals(b); } }; private OnChannelSearchResultSelected listener; - private MuclumbusService.Room current; + private Room current; public ChannelSearchResultAdapter() { super(DIFF); @@ -49,7 +49,7 @@ public class ChannelSearchResultAdapter extends ListAdapter implements CopyTextVie private DisplayMetrics metrics; private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; - private boolean mIndicateReceived = false; private OnQuoteListener onQuoteListener; public MessageAdapter(XmppActivity activity, List messages) { super(activity, 0, messages); @@ -207,9 +206,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie break; case Message.STATUS_SEND_RECEIVED: case Message.STATUS_SEND_DISPLAYED: - if (mIndicateReceived) { - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - } + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); break; case Message.STATUS_SEND_FAILED: final String errorMessage = message.getErrorMessage(); @@ -903,7 +900,6 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie public void updatePreferences() { SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - this.mIndicateReceived = p.getBoolean("indicate_received", activity.getResources().getBoolean(R.bool.indicate_received)); } diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java index 0843c214f..eb45feebe 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -70,8 +70,8 @@ public class FixedURLSpan extends URLSpan { public void onClick(View widget) { final Uri uri = Uri.parse(getURL()); final Context context = widget.getContext(); - final boolean candidateToProcessDirecty = "xmpp".equals(uri.getScheme()) || ("https".equals(uri.getScheme()) && "conversations.im".equals(uri.getHost()) && uri.getPathSegments().size() > 1 && Arrays.asList("j","i").contains(uri.getPathSegments().get(0))); - if (candidateToProcessDirecty && context instanceof ConversationsActivity) { + final boolean candidateToProcessDirectly = "xmpp".equals(uri.getScheme()) || ("https".equals(uri.getScheme()) && "conversations.im".equals(uri.getHost()) && uri.getPathSegments().size() > 1 && Arrays.asList("j","i").contains(uri.getPathSegments().get(0))); + if (candidateToProcessDirectly && context instanceof ConversationsActivity) { if (((ConversationsActivity) context).onXmppUriClicked(uri)) { widget.playSoundEffect(0); return; diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java index c3e874310..04efcc0c5 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java @@ -13,6 +13,7 @@ import android.view.View; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; @@ -71,8 +72,8 @@ public final class MucDetailsContextMenuHelper { startConversation.setVisible(true); final Contact contact = user.getContact(); final User self = conversation.getMucOptions().getSelf(); - if (contact != null && contact.showInRoster()) { - showContactDetails.setVisible(!contact.isSelf()); + if ((contact != null && contact.showInRoster()) || mucOptions.isPrivateAndNonAnonymous()) { + showContactDetails.setVisible(contact == null || !contact.isSelf()); } if ((activity instanceof ConferenceDetailsActivity || activity instanceof MucUsersActivity) && user.getRole() == MucOptions.Role.NONE) { invite.setVisible(true); @@ -135,7 +136,9 @@ public final class MucDetailsContextMenuHelper { Jid jid = user.getRealJid(); switch (item.getItemId()) { case R.id.action_contact_details: - Contact contact = user.getContact(); + final Jid realJid = user.getRealJid(); + final Account account = conversation.getAccount(); + final Contact contact = realJid == null ? null : account.getRoster().getContact(realJid); if (contact != null) { activity.switchToContactDetails(contact, fingerprint); } diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index e475c6718..475682bec 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java @@ -102,7 +102,11 @@ public class AccountUtils { public static void showHideMenuItems(final Menu menu) { final MenuItem manageAccounts = menu.findItem(R.id.action_accounts); final MenuItem manageAccount = menu.findItem(R.id.action_account); - manageAccount.setVisible(MANAGE_ACCOUNT_ACTIVITY == null); - manageAccounts.setVisible(MANAGE_ACCOUNT_ACTIVITY != null); + if (manageAccount != null) { + manageAccount.setVisible(MANAGE_ACCOUNT_ACTIVITY == null); + } + if (manageAccounts != null) { + manageAccounts.setVisible(MANAGE_ACCOUNT_ACTIVITY != null); + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 9e63ee3c3..4a2a14111 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -93,7 +93,7 @@ public class Compatibility { public static void removeUnusedPreferences(SettingsFragment settingsFragment) { List categories = Arrays.asList( (PreferenceCategory) settingsFragment.findPreference("notification_category"), - (PreferenceCategory) settingsFragment.findPreference("other_expert_category")); + (PreferenceCategory) settingsFragment.findPreference("advanced")); for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUESD_SETTINGS_PRE_TWENTYSIX)) { Preference preference = settingsFragment.findPreference(key); if (preference != null) { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 4be004b97..189492fbf 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xml; public final class Namespace { + public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; + public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String BLOCKING = "urn:xmpp:blocking"; public static final String ROSTER = "jabber:iq:roster"; public static final String REGISTER = "jabber:iq:register"; diff --git a/src/main/res/drawable/no_results_primary_background_dark.xml b/src/main/res/drawable/no_results_primary_background_dark.xml new file mode 100644 index 000000000..48597d05c --- /dev/null +++ b/src/main/res/drawable/no_results_primary_background_dark.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/src/main/res/drawable/no_results_primary_background_light.xml b/src/main/res/drawable/no_results_primary_background_light.xml new file mode 100644 index 000000000..efe31413a --- /dev/null +++ b/src/main/res/drawable/no_results_primary_background_light.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/src/main/res/layout/activity_channel_discovery.xml b/src/main/res/layout/activity_channel_discovery.xml index 055b9aff2..34b680622 100644 --- a/src/main/res/layout/activity_channel_discovery.xml +++ b/src/main/res/layout/activity_channel_discovery.xml @@ -34,7 +34,6 @@ android:background="?attr/color_background_primary" android:orientation="vertical" android:scrollbars="vertical" - android:visibility="gone" app:layoutManager="android.support.v7.widget.LinearLayoutManager" /> diff --git a/src/main/res/menu/channel_discovery_activity.xml b/src/main/res/menu/channel_discovery_activity.xml index 209bb27e0..8df3c2d8c 100644 --- a/src/main/res/menu/channel_discovery_activity.xml +++ b/src/main/res/menu/channel_discovery_activity.xml @@ -1,11 +1,21 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:title="@string/search" + app:actionLayout="@layout/actionview_search" + app:showAsAction="collapseActionView|always" /> + + diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index 54e122dad..50ca38c6e 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -62,7 +62,7 @@ @string/never - @string/automatically + @string/large_images_only @string/always @@ -124,4 +124,14 @@ @string/video_720p @string/video_original + + + @string/jabber_network + @string/local_server + + + + JABBER_NETWORK + LOCAL_SERVER + diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 1f78b19a2..ce15d0013 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -22,6 +22,7 @@ + diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index bf543fd4e..a4a1448cd 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -16,7 +16,6 @@ 524288 auto light - true recent true true @@ -31,7 +30,6 @@ true true true - true true false false @@ -42,4 +40,5 @@ false false 360 + JABBER_NETWORK diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 0a9452ecd..15a4e16ea 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -119,7 +119,7 @@ Ringtone Play sound when a new message arrives Grace Period - The length of time Conversations keeps quiet after seeing activity on another device + The length of time notifications are silenced after detecting activity on one of your other devices. Advanced Never send crash reports By sending in stack traces you are helping the ongoing development of Conversations @@ -281,10 +281,6 @@ End time Enable quiet hours Notifications will be silenced during quiet hours - Send button indicates status - Request message receipts - Received messages will be marked with a green tick if supported - Colorize send button to indicate contact status Other Synchronize with bookmarks Join and leave group chats according to auto-join flag in your bookmarks. @@ -512,9 +508,9 @@ Notifications disabled Notifications paused Image Compression - Resize and compress images + Hint: Use ‘Choose file’ instead of ‘Choose picture’ to send individual images uncompressed regardless of this setting. Always - Automatically + Large images only Battery optimizations enabled Your device is doing some heavy battery optimizations on Conversations that might lead to delayed notifications or even message loss.\nIt is recommended to disable those. Your device is doing some heavy battery optimizations on Conversations that might lead to delayed notifications or even message loss.\n\nYou will now be asked to disable those. @@ -876,4 +872,12 @@ Please enter the password for this account Unable to perform this action Join public channel… + The sharing application did not grant permission to access this file. + + jabber.network + Local server + Most users should choose ‘jabber.network’ for better suggestions from the entirety of the public XMPP ecosystem. + Channel discovery method + Backup + About diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 385458c61..72da37bf5 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -18,6 +18,7 @@ @drawable/search_background_light @drawable/no_results_background_light + @drawable/no_results_primary_background_light @drawable/list_item_background_light @color/black @@ -127,6 +128,7 @@ @color/grey700 @drawable/search_background_dark @drawable/no_results_background_dark + @drawable/no_results_primary_background_dark @drawable/list_item_background_dark @color/red_a100 diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 90813d29a..bce0cc349 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -16,6 +16,22 @@ + + + + - - - - - - - - - - + android:key="notification_category" + android:title="@string/pref_notification_settings"> - + - - - + + + - + + + + + + + - + - + + - - - + + + + + + + + - - - @@ -207,11 +214,6 @@ android:key="btbv" android:summary="@string/pref_blind_trust_before_verification_summary" android:title="@string/pref_blind_trust_before_verification" /> - - + + + android:key="group_chats" + android:title="@string/group_chats_and_channels"> - - - + + + + - - - + + +