diff --git a/build.gradle b/build.gradle index 21b73b1bfa..a866675c2f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:3.3.2' classpath "io.realm:realm-gradle-plugin:3.1.1" classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.frogermcs.androiddevmetrics:androiddevmetrics-plugin:0.4' diff --git a/xabber/build.gradle b/xabber/build.gradle index 9d08ac2613..55ad17e937 100644 --- a/xabber/build.gradle +++ b/xabber/build.gradle @@ -10,8 +10,8 @@ android { defaultConfig { minSdkVersion 15 targetSdkVersion 28 - versionCode 591 - versionName '2.6.2(591)' + versionCode 602 + versionName '2.6.3(602)' } lintOptions { diff --git a/xabber/src/main/AndroidManifest.xml b/xabber/src/main/AndroidManifest.xml index cd5135d42e..68d6000156 100644 --- a/xabber/src/main/AndroidManifest.xml +++ b/xabber/src/main/AndroidManifest.xml @@ -68,13 +68,10 @@ + - - - - - + diff --git a/xabber/src/main/java/com/xabber/android/data/ActivityManager.java b/xabber/src/main/java/com/xabber/android/data/ActivityManager.java index 57a99b395b..9c1947ed60 100644 --- a/xabber/src/main/java/com/xabber/android/data/ActivityManager.java +++ b/xabber/src/main/java/com/xabber/android/data/ActivityManager.java @@ -21,6 +21,7 @@ import android.widget.Toast; import com.xabber.android.R; +import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.connection.CertificateManager; import com.xabber.android.data.log.LogManager; import com.xabber.android.ui.activity.AboutActivity; @@ -219,6 +220,7 @@ public void onError(final int resourceId) { application.addUIListener(OnErrorListener.class, onErrorListener); CertificateManager.getInstance().registerActivity(activity); + AccountManager.getInstance().stopGracePeriod(); } /** diff --git a/xabber/src/main/java/com/xabber/android/data/Application.java b/xabber/src/main/java/com/xabber/android/data/Application.java index c2bfdc0be5..6cd0bf4f21 100644 --- a/xabber/src/main/java/com/xabber/android/data/Application.java +++ b/xabber/src/main/java/com/xabber/android/data/Application.java @@ -41,6 +41,7 @@ import com.xabber.android.data.extension.blocking.BlockingManager; import com.xabber.android.data.extension.capability.CapabilitiesManager; import com.xabber.android.data.extension.carbons.CarbonManager; +import com.xabber.android.data.extension.chat_markers.ChatMarkerManager; import com.xabber.android.data.extension.cs.ChatStateManager; import com.xabber.android.data.extension.httpfileupload.HttpFileUploadManager; import com.xabber.android.data.extension.mam.MamManager; @@ -376,6 +377,7 @@ private void addManagers() { addManager(NetworkManager.getInstance()); addManager(ReconnectionManager.getInstance()); addManager(ReceiptManager.getInstance()); + addManager(ChatMarkerManager.getInstance()); addManager(SSNManager.getInstance()); addManager(AttentionManager.getInstance()); addManager(CarbonManager.getInstance()); diff --git a/xabber/src/main/java/com/xabber/android/data/account/AccountItem.java b/xabber/src/main/java/com/xabber/android/data/account/AccountItem.java index 9d4013f864..ee932d25b3 100644 --- a/xabber/src/main/java/com/xabber/android/data/account/AccountItem.java +++ b/xabber/src/main/java/com/xabber/android/data/account/AccountItem.java @@ -44,6 +44,7 @@ public class AccountItem extends ConnectionItem implements Comparable { public static final String UNDEFINED_PASSWORD = ""; + private static final long GRACE_PERIOD = 150000; /** * Id in database. @@ -113,6 +114,8 @@ public class AccountItem extends ConnectionItem implements Comparable System.currentTimeMillis(); + } } diff --git a/xabber/src/main/java/com/xabber/android/data/account/AccountManager.java b/xabber/src/main/java/com/xabber/android/data/account/AccountManager.java index f9b5d44de2..9f3e559ea0 100644 --- a/xabber/src/main/java/com/xabber/android/data/account/AccountManager.java +++ b/xabber/src/main/java/com/xabber/android/data/account/AccountManager.java @@ -1192,4 +1192,20 @@ public void setAllAccountAutoLoginToXabber(boolean autoLogin) { } } + public void startGracePeriod(AccountJid accountJid) { + AccountItem accountItem = getAccount(accountJid); + if (accountItem != null) accountItem.startGracePeriod(); + } + + public void stopGracePeriod(AccountJid accountJid) { + AccountItem accountItem = getAccount(accountJid); + if (accountItem != null) accountItem.stopGracePeriod(); + } + + public void stopGracePeriod() { + for (AccountJid accountJid : AccountManager.getInstance().getEnabledAccounts()) { + stopGracePeriod(accountJid); + } + } + } diff --git a/xabber/src/main/java/com/xabber/android/data/account/ScreenManager.java b/xabber/src/main/java/com/xabber/android/data/account/ScreenManager.java index 14850fc629..378000c390 100644 --- a/xabber/src/main/java/com/xabber/android/data/account/ScreenManager.java +++ b/xabber/src/main/java/com/xabber/android/data/account/ScreenManager.java @@ -21,11 +21,11 @@ import android.content.IntentFilter; import com.xabber.android.data.Application; -import com.xabber.android.data.log.LogManager; import com.xabber.android.data.OnCloseListener; import com.xabber.android.data.OnInitializedListener; import com.xabber.android.data.SettingsManager; import com.xabber.android.data.extension.csi.ClientStateManager; +import com.xabber.android.data.log.LogManager; import com.xabber.android.receiver.GoAwayReceiver; import com.xabber.android.receiver.GoXaReceiver; import com.xabber.android.receiver.ScreenReceiver; @@ -106,6 +106,7 @@ public void onScreen(Intent intent) { alarmManager.cancel(goAwayPendingIntent); alarmManager.cancel(goXaPendingIntent); AccountManager.getInstance().wakeUp(); + AccountManager.getInstance().stopGracePeriod(); // notify server(s) that client is now active ClientStateManager.setActive(); diff --git a/xabber/src/main/java/com/xabber/android/data/database/MessageDatabaseManager.java b/xabber/src/main/java/com/xabber/android/data/database/MessageDatabaseManager.java index f43f64a1ba..3575aad815 100644 --- a/xabber/src/main/java/com/xabber/android/data/database/MessageDatabaseManager.java +++ b/xabber/src/main/java/com/xabber/android/data/database/MessageDatabaseManager.java @@ -19,10 +19,12 @@ import java.util.Date; import io.realm.DynamicRealm; +import io.realm.DynamicRealmObject; import io.realm.FieldAttribute; import io.realm.Realm; import io.realm.RealmConfiguration; import io.realm.RealmMigration; +import io.realm.RealmObjectSchema; import io.realm.RealmQuery; import io.realm.RealmResults; import io.realm.RealmSchema; @@ -31,7 +33,7 @@ public class MessageDatabaseManager { private static final String REALM_MESSAGE_DATABASE_NAME = "xabber.realm"; - static final int REALM_MESSAGE_DATABASE_VERSION = 17; + static final int REALM_MESSAGE_DATABASE_VERSION = 19; private final RealmConfiguration realmConfiguration; private static MessageDatabaseManager instance; @@ -300,6 +302,24 @@ public void migrate(DynamicRealm realm1, long oldVersion, long newVersion) { oldVersion++; } + if (oldVersion == 17) { + schema.get(MessageItem.class.getSimpleName()) + .addField(MessageItem.Fields.DISPLAYED, boolean.class); + + oldVersion++; + } + + if (oldVersion == 18) { + schema.get(MessageItem.class.getSimpleName()) + .transform(new RealmObjectSchema.Function() { + @Override + public void apply(DynamicRealmObject obj) { + obj.setBoolean(MessageItem.Fields.READ, true); + } + }); + oldVersion++; + } + } }) .build(); diff --git a/xabber/src/main/java/com/xabber/android/data/database/RealmManager.java b/xabber/src/main/java/com/xabber/android/data/database/RealmManager.java index 4eefc326c9..50de97fb6e 100644 --- a/xabber/src/main/java/com/xabber/android/data/database/RealmManager.java +++ b/xabber/src/main/java/com/xabber/android/data/database/RealmManager.java @@ -34,7 +34,7 @@ public class RealmManager { private static final String REALM_DATABASE_NAME = "realm_database.realm"; - private static final int REALM_DATABASE_VERSION = 20; + private static final int REALM_DATABASE_VERSION = 21; private static final String LOG_TAG = RealmManager.class.getSimpleName(); private final RealmConfiguration realmConfiguration; @@ -302,10 +302,12 @@ public void migrate(DynamicRealm realm, long oldVersion, long newVersion) { } if (oldVersion == 20) { - schema.create(UploadServer.class.getSimpleName()) - .addField(UploadServer.Fields.ID, String.class, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED) - .addField(UploadServer.Fields.ACCOUNT, String.class) - .addField(UploadServer.Fields.SERVER, String.class); + if (!schema.contains(UploadServer.class.getSimpleName())) { + schema.create(UploadServer.class.getSimpleName()) + .addField(UploadServer.Fields.ID, String.class, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED) + .addField(UploadServer.Fields.ACCOUNT, String.class) + .addField(UploadServer.Fields.SERVER, String.class); + } oldVersion++; } diff --git a/xabber/src/main/java/com/xabber/android/data/database/messagerealm/MessageItem.java b/xabber/src/main/java/com/xabber/android/data/database/messagerealm/MessageItem.java index 10f43c8cfb..b9fddff349 100644 --- a/xabber/src/main/java/com/xabber/android/data/database/messagerealm/MessageItem.java +++ b/xabber/src/main/java/com/xabber/android/data/database/messagerealm/MessageItem.java @@ -54,6 +54,7 @@ public static class Fields { public static final String ERROR = "error"; public static final String ERROR_DESCR = "errorDescription"; public static final String DELIVERED = "delivered"; + public static final String DISPLAYED = "displayed"; public static final String SENT = "sent"; public static final String READ = "read"; public static final String STANZA_ID = "stanzaId"; @@ -131,6 +132,10 @@ public static class Fields { * Receipt was received for sent message. */ private boolean delivered; + /** + * Chat marker was received for sent message. + */ + private boolean displayed; /** * Message was sent. */ @@ -336,6 +341,14 @@ public void setDelivered(boolean delivered) { this.delivered = delivered; } + public boolean isDisplayed() { + return displayed; + } + + public void setDisplayed(boolean displayed) { + this.displayed = displayed; + } + public boolean isSent() { return sent; } diff --git a/xabber/src/main/java/com/xabber/android/data/extension/carbons/CarbonCopyListener.java b/xabber/src/main/java/com/xabber/android/data/extension/carbons/CarbonCopyListener.java index 5e9394fa38..8230fe4d1a 100644 --- a/xabber/src/main/java/com/xabber/android/data/extension/carbons/CarbonCopyListener.java +++ b/xabber/src/main/java/com/xabber/android/data/extension/carbons/CarbonCopyListener.java @@ -3,7 +3,8 @@ import com.xabber.android.data.Application; import com.xabber.android.data.entity.AccountJid; -import com.xabber.android.data.log.LogManager; +import com.xabber.android.data.extension.chat_markers.ChatMarkerManager; +import com.xabber.android.data.extension.cs.ChatStateManager; import com.xabber.android.data.message.MessageManager; import org.jivesoftware.smack.packet.Message; @@ -24,11 +25,12 @@ class CarbonCopyListener implements CarbonCopyReceivedListener { @Override public void onCarbonCopyReceived(final CarbonExtension.Direction direction, final Message carbonCopy, Message wrappingMessage) { - LogManager.i(LOG_TAG, "onCarbonCopyReceived " + direction + " " + carbonCopy.getBody()); Application.getInstance().runOnUiThread(new Runnable() { @Override public void run() { MessageManager.getInstance().processCarbonsMessage(account, carbonCopy, direction); + ChatMarkerManager.getInstance().processCarbonsMessage(account, carbonCopy, direction); + ChatStateManager.getInstance().processCarbonsMessage(account, carbonCopy, direction); } }); } diff --git a/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/BackpressureMessageReader.java b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/BackpressureMessageReader.java new file mode 100644 index 0000000000..5d672dd7db --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/BackpressureMessageReader.java @@ -0,0 +1,101 @@ +package com.xabber.android.data.extension.chat_markers; + +import com.xabber.android.data.database.MessageDatabaseManager; +import com.xabber.android.data.database.messagerealm.MessageItem; +import com.xabber.android.data.log.LogManager; +import com.xabber.android.data.message.AbstractChat; +import com.xabber.android.data.message.MessageManager; +import com.xabber.android.data.roster.AbstractContact; +import com.xabber.android.data.roster.RosterManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.realm.Realm; +import io.realm.RealmResults; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.subjects.PublishSubject; + + +/** Groups messages to send displayed. To avoid + * too often sending displayed chat markers. + * */ +public class BackpressureMessageReader { + + private static BackpressureMessageReader instance; + private Map> queries = new HashMap<>(); + + public static BackpressureMessageReader getInstance() { + if (instance == null) { + instance = new BackpressureMessageReader(); + } + return instance; + } + + public void markAsRead(MessageItem messageItem, boolean trySendDisplayed) { + AbstractContact contact = RosterManager.getInstance().getAbstractContact(messageItem.getAccount(), messageItem.getUser()); + PublishSubject subject = queries.get(contact); + if (subject == null) subject = createSubject(contact); + subject.onNext(new MessageHolder(messageItem, trySendDisplayed)); + } + + private PublishSubject createSubject(final AbstractContact contact) { + PublishSubject subject = PublishSubject.create(); + subject.debounce(2000, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1() { + @Override + public void call(MessageHolder holder) { + MessageItem message = holder.messageItem; + if (holder.trySendDisplayed) + ChatMarkerManager.getInstance().sendDisplayed(message); + + Realm realm = MessageDatabaseManager.getInstance().getRealmUiThread(); + RealmResults messages = getPreviousUnreadMessages(realm, message); + realm.beginTransaction(); + List ids = new ArrayList<>(); + for (MessageItem mes : messages) { + mes.setRead(true); + ids.add(mes.getUniqueId()); + } + realm.commitTransaction(); + + AbstractChat chat = MessageManager.getInstance().getOrCreateChat(message.getAccount(), message.getUser()); + if (chat != null) chat.approveRead(ids); + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + LogManager.exception(this, throwable); + LogManager.d(this, "Exception is thrown. Subject was deleted."); + queries.remove(contact); + } + }); + queries.put(contact, subject); + return subject; + } + + private RealmResults getPreviousUnreadMessages(Realm realm, MessageItem messageItem) { + return realm.where(MessageItem.class) + .equalTo(MessageItem.Fields.ACCOUNT, messageItem.getAccount().toString()) + .equalTo(MessageItem.Fields.USER, messageItem.getUser().toString()) + .equalTo(MessageItem.Fields.READ, false) + .lessThanOrEqualTo(MessageItem.Fields.TIMESTAMP, messageItem.getTimestamp()) + .findAll(); + } + + private class MessageHolder { + final MessageItem messageItem; + final boolean trySendDisplayed; + + public MessageHolder(MessageItem messageItem, boolean trySendDisplayed) { + this.messageItem = messageItem; + this.trySendDisplayed = trySendDisplayed; + } + } + +} diff --git a/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkerManager.java b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkerManager.java new file mode 100644 index 0000000000..3f91ce0874 --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkerManager.java @@ -0,0 +1,212 @@ +package com.xabber.android.data.extension.chat_markers; + +import com.xabber.android.data.NetworkException; +import com.xabber.android.data.account.AccountItem; +import com.xabber.android.data.account.AccountManager; +import com.xabber.android.data.connection.ConnectionItem; +import com.xabber.android.data.connection.StanzaSender; +import com.xabber.android.data.connection.listeners.OnPacketListener; +import com.xabber.android.data.database.MessageDatabaseManager; +import com.xabber.android.data.database.messagerealm.MessageItem; +import com.xabber.android.data.entity.AccountJid; +import com.xabber.android.data.entity.UserJid; +import com.xabber.android.data.extension.chat_markers.filter.ChatMarkersFilter; +import com.xabber.android.data.log.LogManager; +import com.xabber.android.data.message.AbstractChat; +import com.xabber.android.data.message.MessageManager; +import com.xabber.android.data.message.MessageUpdateEvent; +import com.xabber.android.data.notification.MessageNotificationManager; +import com.xabber.android.data.roster.RosterManager; + +import org.greenrobot.eventbus.EventBus; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.MessageTypeFilter; +import org.jivesoftware.smack.filter.MessageWithBodiesFilter; +import org.jivesoftware.smack.filter.NotFilter; +import org.jivesoftware.smack.filter.StanzaFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; +import org.jivesoftware.smackx.chat_markers.element.ChatMarkersElements; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; +import org.jxmpp.jid.Jid; + +import java.util.List; + +import io.realm.Realm; +import io.realm.RealmResults; + +public class ChatMarkerManager implements OnPacketListener { + + private static final StanzaFilter OUTGOING_MESSAGE_FILTER = new AndFilter( + MessageTypeFilter.NORMAL_OR_CHAT, + MessageWithBodiesFilter.INSTANCE, + new NotFilter(ChatMarkersFilter.INSTANCE), + EligibleForChatMarkerFilter.INSTANCE + ); + + private static ChatMarkerManager instance; + + public static ChatMarkerManager getInstance() { + if (instance == null) instance = new ChatMarkerManager(); + return instance; + } + + public ChatMarkerManager() { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(final XMPPConnection connection) { + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(ChatMarkersElements.NAMESPACE); + connection.addPacketInterceptor(new StanzaListener() { + @Override + public void processStanza(Stanza packet) { + Message message = (Message) packet; + message.addExtension(new ChatMarkersElements.MarkableExtension()); + } + }, OUTGOING_MESSAGE_FILTER); + } + }); + } + + @Override + public void onStanza(ConnectionItem connection, Stanza packet) { + if (packet instanceof Message) { + final Message message = (Message) packet; + + if (ChatMarkersElements.MarkableExtension.from(message) != null) { + // markable + sendReceived(message, connection.getAccount()); + } else if (ChatMarkersElements.ReceivedExtension.from(message) != null) { + // received + markAsDelivered(ChatMarkersElements.ReceivedExtension.from(message).getId()); + } else if (ChatMarkersElements.DisplayedExtension.from(message) != null) { + // displayed + markAsDisplayed(ChatMarkersElements.DisplayedExtension.from(message).getId()); + } else if (ChatMarkersElements.AcknowledgedExtension.from(message) != null) { + // acknowledged + } + } + } + + public void sendDisplayed(MessageItem messageItem) { + if (messageItem.getStanzaId() == null || messageItem.getStanzaId().isEmpty()) return; + + Message displayed = new Message(messageItem.getUser().getJid()); + displayed.addExtension(new ChatMarkersElements.DisplayedExtension(messageItem.getStanzaId())); + displayed.setType(Message.Type.chat); + try { + StanzaSender.sendStanza(messageItem.getAccount(), displayed); + } catch (NetworkException e) { + LogManager.exception(this, e); + } + } + + public void processCarbonsMessage(AccountJid account, final Message message, CarbonExtension.Direction direction) { + if (direction == CarbonExtension.Direction.sent) { + ChatMarkersElements.DisplayedExtension extension = + ChatMarkersElements.DisplayedExtension.from(message); + if (extension != null) { + UserJid companion; + try { + companion = UserJid.from(message.getTo()).getBareUserJid(); + } catch (UserJid.UserJidCreateException e) { + return; + } + AbstractChat chat = MessageManager.getInstance().getOrCreateChat(account, companion); + if (chat != null) { + chat.markAsRead(extension.getId(), false); + MessageNotificationManager.getInstance().removeChatWithTimer(account, companion); + + // start grace period + AccountManager.getInstance().startGracePeriod(account); + } + } + } + } + + private boolean isClientSupportChatMarkers(AccountJid account, UserJid user) { + AccountItem accountItem = AccountManager.getInstance().getAccount(account); + if (accountItem == null) return false; + + XMPPConnection connection = accountItem.getConnection(); + final List allPresences = RosterManager.getInstance().getPresences(account, user.getJid()); + boolean isChatMarkersSupport = false; + + for (Presence presence : allPresences) { + Jid fromJid = presence.getFrom(); + DiscoverInfo discoverInfo = null; + try { + discoverInfo = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(fromJid); + } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException | + SmackException.NotConnectedException | InterruptedException | ClassCastException e) { + e.printStackTrace(); + } + + isChatMarkersSupport = discoverInfo != null && discoverInfo.containsFeature(ChatMarkersElements.NAMESPACE); + if (isChatMarkersSupport) break; + } + return isChatMarkersSupport; + } + + private void sendReceived(Message message, AccountJid account) { + if (message.getStanzaId() == null || message.getStanzaId().isEmpty()) return; + + Message received = new Message(message.getFrom()); + received.addExtension(new ChatMarkersElements.ReceivedExtension(message.getStanzaId())); + received.setThread(message.getThread()); + received.setType(Message.Type.chat); + + try { + StanzaSender.sendStanza(account, received); + } catch (NetworkException e) { + LogManager.exception(this, e); + } + } + + private void markAsDisplayed(final String messageID) { + Realm realm = MessageDatabaseManager.getInstance().getRealmUiThread(); + MessageItem first = realm.where(MessageItem.class) + .equalTo(MessageItem.Fields.STANZA_ID, messageID).findFirst(); + + if (first != null) { + RealmResults results = realm.where(MessageItem.class) + .equalTo(MessageItem.Fields.ACCOUNT, first.getAccount().toString()) + .equalTo(MessageItem.Fields.USER, first.getUser().toString()) + .equalTo(MessageItem.Fields.INCOMING, false) + .equalTo(MessageItem.Fields.DISPLAYED, false) + .lessThanOrEqualTo(MessageItem.Fields.TIMESTAMP, first.getTimestamp()) + .findAll(); + + if (results != null) { + realm.beginTransaction(); + for (MessageItem item : results) { + item.setDisplayed(true); + } + realm.commitTransaction(); + EventBus.getDefault().post(new MessageUpdateEvent()); + } + } + } + + private void markAsDelivered(final String stanzaID) { + Realm realm = MessageDatabaseManager.getInstance().getRealmUiThread(); + + MessageItem first = realm.where(MessageItem.class) + .equalTo(MessageItem.Fields.STANZA_ID, stanzaID).findFirst(); + + if (first != null) { + realm.beginTransaction(); + first.setDelivered(true); + realm.commitTransaction(); + } + EventBus.getDefault().post(new MessageUpdateEvent()); + } +} diff --git a/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkersElements.java b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkersElements.java new file mode 100644 index 0000000000..cceef1a677 --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkersElements.java @@ -0,0 +1,182 @@ +package com.xabber.android.data.extension.chat_markers; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.XmlStringBuilder; + + +public class ChatMarkersElements { + + public static final String NAMESPACE = "urn:xmpp:chat-markers:0"; + + /** + * Markable extension class. + * + * @see XEP-0333: Chat + * Markers + * @author Fernando Ramirez + * + */ + public static final class MarkableExtension implements ExtensionElement { + + public static final MarkableExtension INSTANCE = new MarkableExtension(); + + /** + * markable element. + */ + public static final String ELEMENT = ChatMarkersState.markable.toString(); + + private MarkableExtension() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.closeEmptyElement(); + return xml; + } + + public static MarkableExtension from(Message message) { + return (MarkableExtension) message.getExtension(ELEMENT, NAMESPACE); + } + } + + protected abstract static class ChatMarkerExtensionWithId implements ExtensionElement { + protected final String id; + + protected ChatMarkerExtensionWithId(String id) { + this.id = StringUtils.requireNotNullOrEmpty(id, "Message ID must not be null"); + } + + /** + * Get the id. + * + * @return the id + */ + public final String getId() { + return id; + } + + @Override + public final XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute("id", id); + xml.closeEmptyElement(); + return xml; + } + } + + /** + * Received extension class. + * + * @see XEP-0333: Chat + * Markers + * @author Fernando Ramirez + * + */ + public static class ReceivedExtension extends ChatMarkerExtensionWithId { + + /** + * received element. + */ + public static final String ELEMENT = ChatMarkersState.received.toString(); + + public ReceivedExtension(String id) { + super(id); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + public static ReceivedExtension from(Message message) { + return (ReceivedExtension) message.getExtension(ELEMENT, NAMESPACE); + } + } + + /** + * Displayed extension class. + * + * @see XEP-0333: Chat + * Markers + * @author Fernando Ramirez + * + */ + public static class DisplayedExtension extends ChatMarkerExtensionWithId { + + /** + * displayed element. + */ + public static final String ELEMENT = ChatMarkersState.displayed.toString(); + + public DisplayedExtension(String id) { + super(id); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + public static DisplayedExtension from(Message message) { + return (DisplayedExtension) message.getExtension(ELEMENT, NAMESPACE); + } + } + + /** + * Acknowledged extension class. + * + * @see XEP-0333: Chat + * Markers + * @author Fernando Ramirez + * + */ + public static class AcknowledgedExtension extends ChatMarkerExtensionWithId { + + /** + * acknowledged element. + */ + public static final String ELEMENT = ChatMarkersState.acknowledged.toString(); + + public AcknowledgedExtension(String id) { + super(id); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + public static AcknowledgedExtension from(Message message) { + return (AcknowledgedExtension) message.getExtension(ELEMENT, NAMESPACE); + } + } + +} diff --git a/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkersState.java b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkersState.java new file mode 100644 index 0000000000..3fe061e3e0 --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/ChatMarkersState.java @@ -0,0 +1,22 @@ +package com.xabber.android.data.extension.chat_markers; + +public enum ChatMarkersState { + /** + * Indicates that a message can be marked with a Chat Marker and is therefore + * a "markable message". + */ + markable, + /** + * The message has been received by a client. + */ + received, + /** + * The message has been displayed to a user in a active chat and not in a system notification. + */ + displayed, + /** + * The message has been acknowledged by some user interaction e.g. pressing an + * acknowledgement button. + */ + acknowledged +} \ No newline at end of file diff --git a/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/EligibleForChatMarkerFilter.java b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/EligibleForChatMarkerFilter.java new file mode 100644 index 0000000000..cc18070328 --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/EligibleForChatMarkerFilter.java @@ -0,0 +1,50 @@ +package com.xabber.android.data.extension.chat_markers; + +import org.jivesoftware.smack.filter.StanzaExtensionFilter; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.chatstates.ChatState; +import org.jivesoftware.smackx.chatstates.ChatStateManager; + +public final class EligibleForChatMarkerFilter extends StanzaExtensionFilter { + + public static final EligibleForChatMarkerFilter INSTANCE = new EligibleForChatMarkerFilter(ChatStateManager.NAMESPACE); + + private EligibleForChatMarkerFilter(String namespace) { + super(namespace); + } + + /** + * From XEP-0333, Protocol Format: The Chat Marker MUST have an 'id' which is the 'id' of the + * message being marked.
+ * In order to make Chat Markers works together with XEP-0085 as it said in + * 8.5 Interaction with Chat States, only messages with active chat + * state are accepted. + * + * @param message to be analyzed. + * @return true if the message contains a stanza Id. + * @see XEP-0333: Chat Markers + */ + @Override + public boolean accept(Stanza message) { + if (!message.hasStanzaIdSet()) { + return false; + } + + if (super.accept(message)) { + ExtensionElement extension = message.getExtension(ChatStateManager.NAMESPACE); + String chatStateElementName = extension.getElementName(); + + ChatState state; + try { + state = ChatState.valueOf(chatStateElementName); + return (state == ChatState.active); + } + catch (Exception ex) { + return false; + } + } + + return true; + } +} diff --git a/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/filter/ChatMarkersFilter.java b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/filter/ChatMarkersFilter.java new file mode 100644 index 0000000000..880bc6d28d --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/chat_markers/filter/ChatMarkersFilter.java @@ -0,0 +1,15 @@ +package com.xabber.android.data.extension.chat_markers.filter; + +import com.xabber.android.data.extension.chat_markers.ChatMarkersElements; + +import org.jivesoftware.smack.filter.StanzaExtensionFilter; +import org.jivesoftware.smack.filter.StanzaFilter; + +public final class ChatMarkersFilter extends StanzaExtensionFilter { + + public static final StanzaFilter INSTANCE = new ChatMarkersFilter(ChatMarkersElements.NAMESPACE); + + private ChatMarkersFilter(String namespace) { + super(namespace); + } +} diff --git a/xabber/src/main/java/com/xabber/android/data/extension/cs/ChatStateManager.java b/xabber/src/main/java/com/xabber/android/data/extension/cs/ChatStateManager.java index 3f9c00ccdd..54bad9602d 100644 --- a/xabber/src/main/java/com/xabber/android/data/extension/cs/ChatStateManager.java +++ b/xabber/src/main/java/com/xabber/android/data/extension/cs/ChatStateManager.java @@ -19,12 +19,14 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; +import android.util.Log; import com.xabber.android.data.Application; import com.xabber.android.data.NetworkException; import com.xabber.android.data.OnCloseListener; import com.xabber.android.data.SettingsManager; import com.xabber.android.data.account.AccountItem; +import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.connection.ConnectionItem; import com.xabber.android.data.connection.ConnectionManager; import com.xabber.android.data.connection.StanzaSender; @@ -48,12 +50,15 @@ import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence.Type; import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; +import org.jivesoftware.smackx.chatstates.provider.ChatStateExtensionProvider; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; import java.util.Calendar; import java.util.Map; @@ -365,4 +370,17 @@ public void onClose() { pauseIntents.clear(); } + public void processCarbonsMessage(AccountJid account, final Message message, CarbonExtension.Direction direction) { + if (direction == CarbonExtension.Direction.sent) { + for (ExtensionElement extension : message.getExtensions()) + if (extension instanceof ChatStateExtension) { + ChatState chatState = ((ChatStateExtension) extension).getChatState(); + if (chatState == ChatState.active || chatState == ChatState.composing) { + AccountManager.getInstance().startGracePeriod(account); + } + break; + } + } + } + } diff --git a/xabber/src/main/java/com/xabber/android/data/extension/file/FileManager.java b/xabber/src/main/java/com/xabber/android/data/extension/file/FileManager.java index 72a4908fc1..fd18cf7afe 100644 --- a/xabber/src/main/java/com/xabber/android/data/extension/file/FileManager.java +++ b/xabber/src/main/java/com/xabber/android/data/extension/file/FileManager.java @@ -66,7 +66,7 @@ public static boolean fileIsImage(File file) { return extensionIsImage(extractRelevantExtension(file.getPath())); } - private static boolean extensionIsImage(String extension) { + public static boolean extensionIsImage(String extension) { return Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension); } diff --git a/xabber/src/main/java/com/xabber/android/data/extension/file/UriUtils.java b/xabber/src/main/java/com/xabber/android/data/extension/file/UriUtils.java new file mode 100644 index 0000000000..1f2a5a1168 --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/extension/file/UriUtils.java @@ -0,0 +1,63 @@ +package com.xabber.android.data.extension.file; + +import android.database.Cursor; +import android.net.Uri; +import android.provider.OpenableColumns; +import android.webkit.MimeTypeMap; + +import com.xabber.android.data.Application; + +import org.apache.commons.io.FilenameUtils; + +import java.util.UUID; + +public class UriUtils { + + public static boolean uriIsImage(Uri uri) { + return FileManager.extensionIsImage(getExtensionFromUri(uri)); + } + + public static String getFullFileName(Uri uri) { + String extension = getExtensionFromUri(uri); + String name = getFileName(uri); + if (name == null) name = UUID.randomUUID().toString(); + else name = name.replace(".", ""); + String fileName = name + "." + extension; + return fileName; + } + + public static String getMimeType(Uri uri) { + String type = Application.getInstance().getContentResolver().getType(uri); + if (type == null || type.isEmpty()) type = "*/*"; + return type; + } + + private static String getExtensionFromUri(Uri uri) { + String mimeType = Application.getInstance().getContentResolver().getType(uri); + return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + } + + private static String getFileName(Uri uri) { + String result = null; + if (uri.getScheme().equals("content")) { + Cursor cursor = Application.getInstance().getContentResolver() + .query(uri, null, null, null, null); + try { + if (cursor != null && cursor.moveToFirst()) { + result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + } + } finally { + cursor.close(); + } + } + if (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) { + result = result.substring(cut + 1); + } + } + return FilenameUtils.getBaseName(result); + } + +} diff --git a/xabber/src/main/java/com/xabber/android/data/extension/httpfileupload/HttpFileUploadManager.java b/xabber/src/main/java/com/xabber/android/data/extension/httpfileupload/HttpFileUploadManager.java index 9caa8f6f27..4f0a054951 100644 --- a/xabber/src/main/java/com/xabber/android/data/extension/httpfileupload/HttpFileUploadManager.java +++ b/xabber/src/main/java/com/xabber/android/data/extension/httpfileupload/HttpFileUploadManager.java @@ -341,10 +341,9 @@ public void run() { Realm realm = RealmManager.getInstance().getNewRealm(); UploadServer item = realm.where(UploadServer.class) .equalTo(UploadServer.Fields.ACCOUNT, account.toString()).findFirst(); + realm.beginTransaction(); if (item == null) item = new UploadServer(account, server); else item.setServer(server); - - realm.beginTransaction(); realm.copyToRealmOrUpdate(item); realm.commitTransaction(); } diff --git a/xabber/src/main/java/com/xabber/android/data/extension/mam/MamManager.java b/xabber/src/main/java/com/xabber/android/data/extension/mam/MamManager.java index f2360ba1e5..f3dd41fe05 100644 --- a/xabber/src/main/java/com/xabber/android/data/extension/mam/MamManager.java +++ b/xabber/src/main/java/com/xabber/android/data/extension/mam/MamManager.java @@ -135,7 +135,8 @@ private boolean updateIsSupported(AccountItem accountItem) { } } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException - | InterruptedException | SmackException.NotConnectedException | SmackException.NotLoggedInException e) { + | InterruptedException | SmackException.NotConnectedException | SmackException.NotLoggedInException + | ClassCastException e) { LogManager.exception(this, e); return false; } diff --git a/xabber/src/main/java/com/xabber/android/data/extension/muc/MUCManager.java b/xabber/src/main/java/com/xabber/android/data/extension/muc/MUCManager.java index a2c103846c..9b563a793b 100644 --- a/xabber/src/main/java/com/xabber/android/data/extension/muc/MUCManager.java +++ b/xabber/src/main/java/com/xabber/android/data/extension/muc/MUCManager.java @@ -116,7 +116,6 @@ public void run() { ChatData chatData = ChatManager.getInstance().loadChatDataFromRealm(roomChat); if (chatData != null) { roomChat.setLastPosition(chatData.getLastPosition()); - roomChat.setUnreadMessageCount(chatData.getUnreadCount()); roomChat.setArchived(chatData.isArchived(), false); roomChat.setNotificationState(chatData.getNotificationState(), false); } diff --git a/xabber/src/main/java/com/xabber/android/data/message/AbstractChat.java b/xabber/src/main/java/com/xabber/android/data/message/AbstractChat.java index 1c5db16099..57d1974d96 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/AbstractChat.java +++ b/xabber/src/main/java/com/xabber/android/data/message/AbstractChat.java @@ -14,6 +14,7 @@ */ package com.xabber.android.data.message; +import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -32,8 +33,10 @@ import com.xabber.android.data.entity.BaseEntity; import com.xabber.android.data.entity.UserJid; import com.xabber.android.data.extension.carbons.CarbonManager; +import com.xabber.android.data.extension.chat_markers.BackpressureMessageReader; import com.xabber.android.data.extension.cs.ChatStateManager; import com.xabber.android.data.extension.file.FileManager; +import com.xabber.android.data.extension.file.UriUtils; import com.xabber.android.data.extension.forward.ForwardComment; import com.xabber.android.data.extension.httpfileupload.ExtendedFormField; import com.xabber.android.data.extension.httpfileupload.HttpFileUploadManager; @@ -61,13 +64,16 @@ import java.io.File; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import io.realm.Realm; import io.realm.RealmChangeListener; import io.realm.RealmList; +import io.realm.RealmQuery; import io.realm.RealmResults; import io.realm.Sort; @@ -102,10 +108,11 @@ public abstract class AbstractChat extends BaseEntity implements RealmChangeList private String threadId; private int lastPosition; - private int unreadMessageCount; private boolean archived; protected NotificationState notificationState; + private Set waitToMarkAsRead = new HashSet<>(); + private boolean isPrivateMucChat; private boolean isPrivateMucChatAccepted; @@ -296,7 +303,7 @@ protected void createAndSaveNewMessage(boolean ui, String uid, Resourcepart reso originalStanza, parentMessageId, originalFrom, forwardIds, fromMUC, fromMAM); saveMessageItem(ui, messageItem); - EventBus.getDefault().post(new NewMessageEvent()); + //EventBus.getDefault().post(new NewMessageEvent()); } protected void createAndSaveFileMessage(boolean ui, String uid, Resourcepart resource, String text, final ChatAction action, @@ -310,23 +317,24 @@ protected void createAndSaveFileMessage(boolean ui, String uid, Resourcepart res originalStanza, parentMessageId, originalFrom, null, fromMUC, fromMAM); saveMessageItem(ui, messageItem); - EventBus.getDefault().post(new NewMessageEvent()); + //EventBus.getDefault().post(new NewMessageEvent()); } public void saveMessageItem(boolean ui, final MessageItem messageItem) { - final long startTime = System.currentTimeMillis(); - Realm realm; - if (ui) realm = MessageDatabaseManager.getInstance().getRealmUiThread(); - else realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); - - realm.executeTransactionAsync(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - realm.copyToRealm(messageItem); - LogManager.d("REALM", Thread.currentThread().getName() - + " save message item: " + (System.currentTimeMillis() - startTime)); - } - }); + if (ui) BackpressureMessageSaver.getInstance().saveMessageItem(messageItem); + else { + final long startTime = System.currentTimeMillis(); + Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); + realm.executeTransactionAsync(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + realm.copyToRealm(messageItem); + LogManager.d("REALM", Thread.currentThread().getName() + + " save message item: " + (System.currentTimeMillis() - startTime)); + EventBus.getDefault().post(new NewMessageEvent()); + } + }); + } } protected MessageItem createMessageItem(Resourcepart resource, String text, ChatAction action, @@ -347,7 +355,7 @@ protected MessageItem createMessageItem(String uid, Resourcepart resource, Strin RealmList forwardIds, boolean fromMUC, boolean fromMAM) { final boolean visible = MessageManager.getInstance().isVisibleChat(this); - boolean read = incoming ? visible : true; + boolean read = !incoming; boolean send = incoming; if (action == null && text == null) { throw new IllegalArgumentException(); @@ -419,16 +427,8 @@ protected MessageItem createMessageItem(String uid, Resourcepart resource, Strin if (notify && notifyAboutMessage() && !visible) NotificationManager.getInstance().onMessageNotification(messageItem); - // unread message count - if (!visible && action == null) { - if (incoming && !fromMAM) increaseUnreadMessageCount(); - else resetUnreadMessageCount(); - } - // remove notifications if get outgoing message with 2 sec delay - if (!incoming) { - MessageNotificationManager.getInstance().removeChatWithTimer(account, user); - } + if (!incoming) MessageNotificationManager.getInstance().removeChatWithTimer(account, user); // when getting new message, unarchive chat if chat not muted if (this.notifyAboutMessage()) @@ -437,33 +437,16 @@ protected MessageItem createMessageItem(String uid, Resourcepart resource, Strin return messageItem; } - public String newFileMessage(final List files) { + public String newFileMessage(final List files, final List uris) { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); - final String messageId = UUID.randomUUID().toString(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { - - RealmList attachments = new RealmList<>(); - for (File file : files) { - Attachment attachment = new Attachment(); - attachment.setFilePath(file.getPath()); - attachment.setFileSize(file.length()); - attachment.setTitle(file.getName()); - attachment.setIsImage(FileManager.fileIsImage(file)); - attachment.setMimeType(HttpFileUploadManager.getMimeType(file.getPath())); - attachment.setDuration((long) 0); - - if (attachment.isImage()) { - HttpFileUploadManager.ImageSize imageSize = - HttpFileUploadManager.getImageSizes(file.getPath()); - attachment.setImageHeight(imageSize.getHeight()); - attachment.setImageWidth(imageSize.getWidth()); - } - attachments.add(attachment); - } + RealmList attachments; + if (files != null) attachments = attachmentsFromFiles(files); + else attachments = attachmentsFromUris(uris); MessageItem messageItem = new MessageItem(messageId); messageItem.setAccount(account); @@ -484,6 +467,41 @@ public void execute(Realm realm) { return messageId; } + public RealmList attachmentsFromFiles(List files) { + RealmList attachments = new RealmList<>(); + for (File file : files) { + Attachment attachment = new Attachment(); + attachment.setFilePath(file.getPath()); + attachment.setFileSize(file.length()); + attachment.setTitle(file.getName()); + attachment.setIsImage(FileManager.fileIsImage(file)); + attachment.setMimeType(HttpFileUploadManager.getMimeType(file.getPath())); + attachment.setDuration((long) 0); + + if (attachment.isImage()) { + HttpFileUploadManager.ImageSize imageSize = + HttpFileUploadManager.getImageSizes(file.getPath()); + attachment.setImageHeight(imageSize.getHeight()); + attachment.setImageWidth(imageSize.getWidth()); + } + attachments.add(attachment); + } + return attachments; + } + + public RealmList attachmentsFromUris(List uris) { + RealmList attachments = new RealmList<>(); + for (Uri uri : uris) { + Attachment attachment = new Attachment(); + attachment.setTitle(UriUtils.getFullFileName(uri)); + attachment.setIsImage(UriUtils.uriIsImage(uri)); + attachment.setMimeType(UriUtils.getMimeType(uri)); + attachment.setDuration((long) 0); + attachments.add(attachment); + } + return attachments; + } + /** * @return Whether chat accepts packets from specified user. */ @@ -787,24 +805,75 @@ public void onChange(RealmResults messageItems) { updateLastMessage(); } + /** UNREAD MESSAGES */ + + public String getFirstUnreadMessageId() { + String id = null; + RealmResults results = getAllUnreadAscending(); + if (results != null && !results.isEmpty()) { + MessageItem firstUnreadMessage = results.first(); + if (firstUnreadMessage != null) + id = firstUnreadMessage.getUniqueId(); + } + return id; + } + public int getUnreadMessageCount() { - return unreadMessageCount; + int unread = ((int) getAllUnreadQuery().count()) - waitToMarkAsRead.size(); + if (unread < 0) unread = 0; + return unread; } - public void increaseUnreadMessageCount() { - this.unreadMessageCount++; - ChatManager.getInstance().saveOrUpdateChatDataToRealm(this); + public void approveRead(List ids) { + for (String id : ids) { + waitToMarkAsRead.remove(id); + } + EventBus.getDefault().post(new MessageUpdateEvent(account, user)); } - public void resetUnreadMessageCount() { - this.unreadMessageCount = 0; - ChatManager.getInstance().saveOrUpdateChatDataToRealm(this); + public void markAsRead(String messageId, boolean trySendDisplay) { + MessageItem message = MessageDatabaseManager.getInstance().getRealmUiThread() + .where(MessageItem.class).equalTo(MessageItem.Fields.STANZA_ID, messageId).findFirst(); + if (message != null) executeRead(message, trySendDisplay); + } + + public void markAsRead(MessageItem messageItem, boolean trySendDisplay) { + waitToMarkAsRead.add(messageItem.getUniqueId()); + executeRead(messageItem, trySendDisplay); + } + + public void markAsReadAll(boolean trySendDisplay) { + RealmResults results = getAllUnreadAscending(); + if (results != null && !results.isEmpty()) { + for (MessageItem message : results) { + waitToMarkAsRead.add(message.getUniqueId()); + } + MessageItem lastMessage = results.last(); + if (lastMessage != null) executeRead(lastMessage, trySendDisplay); + } } - public void setUnreadMessageCount(int unreadMessageCount) { - this.unreadMessageCount = unreadMessageCount; + private void executeRead(MessageItem messageItem, boolean trySendDisplay) { + EventBus.getDefault().post(new MessageUpdateEvent(account, user)); + BackpressureMessageReader.getInstance().markAsRead(messageItem, trySendDisplay); } + private RealmQuery getAllUnreadQuery() { + return MessageDatabaseManager.getInstance().getRealmUiThread().where(MessageItem.class) + .equalTo(MessageItem.Fields.ACCOUNT, account.toString()) + .equalTo(MessageItem.Fields.USER, user.toString()) + .isNull(MessageItem.Fields.PARENT_MESSAGE_ID) + .isNotNull(MessageItem.Fields.TEXT) + .equalTo(MessageItem.Fields.INCOMING, true) + .equalTo(MessageItem.Fields.READ, false); + } + + private RealmResults getAllUnreadAscending() { + return getAllUnreadQuery().findAllSorted(MessageItem.Fields.TIMESTAMP, Sort.ASCENDING); + } + + /** ^ UNREAD MESSAGES ^ */ + public boolean isArchived() { return archived; } diff --git a/xabber/src/main/java/com/xabber/android/data/message/BackpressureMessageSaver.java b/xabber/src/main/java/com/xabber/android/data/message/BackpressureMessageSaver.java new file mode 100644 index 0000000000..8e07d4e1d3 --- /dev/null +++ b/xabber/src/main/java/com/xabber/android/data/message/BackpressureMessageSaver.java @@ -0,0 +1,77 @@ +package com.xabber.android.data.message; + +import com.xabber.android.data.database.MessageDatabaseManager; +import com.xabber.android.data.database.messagerealm.MessageItem; +import com.xabber.android.data.log.LogManager; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.realm.Realm; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.subjects.PublishSubject; + + +/** Groups messages to save. It is necessary to avoid + * java.util.concurrent.RejectedExecutionException error, + * which can occur if you frequently save messages one at a time in Realm. + * + * Issue in crashlytics: https://www.fabric.io/redsolution/android/apps/com.xabber.android/issues/5c55e9edf8b88c29636f3fd3 + * */ +public class BackpressureMessageSaver { + + private static BackpressureMessageSaver instance; + private PublishSubject subject; + + public static BackpressureMessageSaver getInstance() { + if (instance == null) instance = new BackpressureMessageSaver(); + return instance; + } + + public void saveMessageItem(MessageItem messageItem) { + subject.onNext(messageItem); + } + + private BackpressureMessageSaver() { + createSubject(); + } + + private void createSubject() { + subject = PublishSubject.create(); + subject.buffer(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Action1>() { + @Override + public void call(final List messageItems) { + if (messageItems == null || messageItems.isEmpty()) return; + try { + Realm realm = MessageDatabaseManager.getInstance().getRealmUiThread(); + realm.executeTransactionAsync(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + realm.copyToRealm(messageItems); + } + }, new Realm.Transaction.OnSuccess() { + @Override + public void onSuccess() { + EventBus.getDefault().post(new NewMessageEvent()); + } + }); + } catch (Exception e) { + LogManager.exception(this, e); + } + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + LogManager.exception(this, throwable); + LogManager.d(this, "Exception is thrown. Created new publish subject."); + createSubject(); + } + }); + } + +} diff --git a/xabber/src/main/java/com/xabber/android/data/message/ChatData.java b/xabber/src/main/java/com/xabber/android/data/message/ChatData.java index 60e72efce0..de5884abad 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/ChatData.java +++ b/xabber/src/main/java/com/xabber/android/data/message/ChatData.java @@ -9,17 +9,15 @@ public class ChatData { private String subject; private String accountJid; private String userJid; - private int unreadCount; private boolean archived; private NotificationState notificationState; private int lastPosition; - public ChatData(String subject, String accountJid, String userJid, int unreadCount, + public ChatData(String subject, String accountJid, String userJid, boolean archived, NotificationState notificationState, int lastPosition) { this.subject = subject; this.accountJid = accountJid; this.userJid = userJid; - this.unreadCount = unreadCount; this.archived = archived; this.notificationState = notificationState; this.lastPosition = lastPosition; @@ -49,14 +47,6 @@ public void setUserJid(String userJid) { this.userJid = userJid; } - public int getUnreadCount() { - return unreadCount; - } - - public void setUnreadCount(int unreadCount) { - this.unreadCount = unreadCount; - } - public boolean isArchived() { return archived; } diff --git a/xabber/src/main/java/com/xabber/android/data/message/ForwardManager.java b/xabber/src/main/java/com/xabber/android/data/message/ForwardManager.java index 6cedcb923c..3b259bd9f9 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/ForwardManager.java +++ b/xabber/src/main/java/com/xabber/android/data/message/ForwardManager.java @@ -35,10 +35,10 @@ public static void forwardMessage(List messages, AccountJid account, Use @Override public void execute(Realm realm) { realm.copyToRealm(messageItem); + EventBus.getDefault().post(new NewMessageEvent()); chat.sendMessages(); } }); - EventBus.getDefault().post(new NewMessageEvent()); } public static String parseForwardComment(Stanza packet) { diff --git a/xabber/src/main/java/com/xabber/android/data/message/MessageManager.java b/xabber/src/main/java/com/xabber/android/data/message/MessageManager.java index 4e1fbba776..4c5774ef8d 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/MessageManager.java +++ b/xabber/src/main/java/com/xabber/android/data/message/MessageManager.java @@ -14,6 +14,7 @@ */ package com.xabber.android.data.message; +import android.net.Uri; import android.os.Environment; import android.os.Looper; import android.support.annotation.Nullable; @@ -59,7 +60,6 @@ import com.xabber.android.data.roster.RosterManager; import com.xabber.android.utils.StringUtils; -import org.greenrobot.eventbus.EventBus; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; @@ -202,7 +202,6 @@ private RegularChat createChat(AccountJid account, UserJid user) { ChatData chatData = ChatManager.getInstance().loadChatDataFromRealm(chat); if (chatData != null) { chat.setLastPosition(chatData.getLastPosition()); - chat.setUnreadMessageCount(chatData.getUnreadCount()); chat.setArchived(chatData.isArchived(), false); chat.setNotificationState(chatData.getNotificationState(), false); } @@ -215,7 +214,6 @@ private RegularChat createPrivateMucChat(AccountJid account, FullJid fullJid) th ChatData chatData = ChatManager.getInstance().loadChatDataFromRealm(chat); if (chatData != null) { chat.setLastPosition(chatData.getLastPosition()); - chat.setUnreadMessageCount(chatData.getUnreadCount()); chat.setArchived(chatData.isArchived(), false); chat.setNotificationState(chatData.getNotificationState(), false); } @@ -256,30 +254,37 @@ public void removeChat(AbstractChat chat) { public void sendMessage(AccountJid account, UserJid user, String text) { AbstractChat chat = getOrCreateChat(account, user); sendMessage(text, chat); + + // stop grace period + AccountManager.getInstance().stopGracePeriod(account); } private void sendMessage(final String text, final AbstractChat chat) { - final long startTime = System.currentTimeMillis(); - MessageDatabaseManager.getInstance().getRealmUiThread() .executeTransactionAsync(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem newMessageItem = chat.createNewMessageItem(text); realm.copyToRealm(newMessageItem); - LogManager.d("REALM", Thread.currentThread().getName() - + " save message before sending: " + (System.currentTimeMillis() - startTime)); if (chat.canSendMessage()) chat.sendMessages(); } }); + + // mark incoming messages as read + chat.markAsReadAll(true); } public String createFileMessage(AccountJid account, UserJid user, List files) { AbstractChat chat = getOrCreateChat(account, user); + chat.openChat(); + return chat.newFileMessage(files, null); + } + public String createFileMessageFromUris(AccountJid account, UserJid user, List uris) { + AbstractChat chat = getOrCreateChat(account, user); chat.openChat(); - return chat.newFileMessage(files); + return chat.newFileMessage(null, uris); } public void updateFileMessage(AccountJid account, UserJid user, final String messageId, @@ -346,6 +351,10 @@ public void execute(Realm realm) { if (messageItem != null) { RealmList attachments = messageItem.getAttachments(); + // remove temporary attachments created from uri + // to replace it with attachments created from files + attachments.deleteAllFromRealm(); + for (File file : files) { Attachment attachment = new Attachment(); attachment.setFilePath(file.getPath()); @@ -511,30 +520,8 @@ public void closeChat(AccountJid account, UserJid user) { */ public void setVisibleChat(BaseEntity visibleChat) { AbstractChat chat = getChat(visibleChat.getAccount(), visibleChat.getUser()); - if (chat == null) { + if (chat == null) chat = createChat(visibleChat.getAccount(), visibleChat.getUser()); - } else { - final AccountJid account = chat.getAccount(); - final UserJid user = chat.getUser(); - - MessageDatabaseManager.getInstance() - .getRealmUiThread().executeTransactionAsync(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - RealmResults unreadMessages = realm.where(MessageItem.class) - .equalTo(MessageItem.Fields.ACCOUNT, account.toString()) - .equalTo(MessageItem.Fields.USER, user.toString()) - .equalTo(MessageItem.Fields.READ, false) - .findAll(); - - List unreadMessagesList = new ArrayList<>(unreadMessages); - - for (MessageItem messageItem : unreadMessagesList) { - messageItem.setRead(true); - } - } - }); - } this.visibleChat = chat; } @@ -787,37 +774,36 @@ public void processCarbonsMessage(AccountJid account, final Message message, Car final AbstractChat finalChat = chat; - MessageDatabaseManager.getInstance().getRealmUiThread() - .executeTransactionAsync(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - String text = body; - String uid = UUID.randomUUID().toString(); - RealmList forwardIds = finalChat.parseForwardedMessage(false, message, uid); - String originalStanza = message.toXML().toString(); - String originalFrom = message.getFrom().toString(); - String forwardComment = ForwardManager.parseForwardComment(message); - if (forwardComment != null) text = forwardComment; - - MessageItem newMessageItem = finalChat.createNewMessageItem(text); - newMessageItem.setStanzaId(message.getStanzaId()); - newMessageItem.setSent(true); - newMessageItem.setForwarded(true); - - // forwarding - if (forwardIds != null) newMessageItem.setForwardedIds(forwardIds); - newMessageItem.setOriginalStanza(originalStanza); - newMessageItem.setOriginalFrom(originalFrom); - - // attachments - RealmList attachments = HttpFileUploadManager.parseFileMessage(message); - if (attachments.size() > 0) - newMessageItem.setAttachments(attachments); - - realm.copyToRealm(newMessageItem); - EventBus.getDefault().post(new NewMessageEvent()); - } - }); + String text = body; + String uid = UUID.randomUUID().toString(); + RealmList forwardIds = finalChat.parseForwardedMessage(true, message, uid); + String originalStanza = message.toXML().toString(); + String originalFrom = message.getFrom().toString(); + String forwardComment = ForwardManager.parseForwardComment(message); + if (forwardComment != null) text = forwardComment; + + MessageItem newMessageItem = finalChat.createNewMessageItem(text); + newMessageItem.setStanzaId(message.getStanzaId()); + newMessageItem.setSent(true); + newMessageItem.setForwarded(true); + + // forwarding + if (forwardIds != null) newMessageItem.setForwardedIds(forwardIds); + newMessageItem.setOriginalStanza(originalStanza); + newMessageItem.setOriginalFrom(originalFrom); + + // attachments + RealmList attachments = HttpFileUploadManager.parseFileMessage(message); + if (attachments.size() > 0) + newMessageItem.setAttachments(attachments); + + BackpressureMessageSaver.getInstance().saveMessageItem(newMessageItem); + + // mark incoming messages as read + finalChat.markAsReadAll(false); + + // start grace period + AccountManager.getInstance().startGracePeriod(account); return; } diff --git a/xabber/src/main/java/com/xabber/android/data/message/ReceiptManager.java b/xabber/src/main/java/com/xabber/android/data/message/ReceiptManager.java index 803c95f1fb..fe2601f2a9 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/ReceiptManager.java +++ b/xabber/src/main/java/com/xabber/android/data/message/ReceiptManager.java @@ -69,7 +69,7 @@ private ReceiptManager() { @Override public void connectionCreated(final XMPPConnection connection) { DeliveryReceiptManager.getInstanceFor(connection).addReceiptReceivedListener(ReceiptManager.this); - DeliveryReceiptManager.getInstanceFor(connection).autoAddDeliveryReceiptRequests(); + //DeliveryReceiptManager.getInstanceFor(connection).autoAddDeliveryReceiptRequests(); } }); @@ -108,6 +108,7 @@ public void run() { receipt.addExtension(new DeliveryReceipt(id)); // the key problem is Thread - smack does not keep it in auto reply receipt.setThread(message.getThread()); + receipt.setType(Message.Type.chat); try { StanzaSender.sendStanza(account, receipt); } catch (NetworkException e) { diff --git a/xabber/src/main/java/com/xabber/android/data/message/RegularChat.java b/xabber/src/main/java/com/xabber/android/data/message/RegularChat.java index c9f608588f..05b6d9bcb4 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/RegularChat.java +++ b/xabber/src/main/java/com/xabber/android/data/message/RegularChat.java @@ -25,6 +25,7 @@ import com.xabber.android.data.database.messagerealm.MessageItem; import com.xabber.android.data.entity.AccountJid; import com.xabber.android.data.entity.UserJid; +import com.xabber.android.data.extension.chat_markers.ChatMarkerManager; import com.xabber.android.data.extension.httpfileupload.HttpFileUploadManager; import com.xabber.android.data.extension.muc.MUCManager; import com.xabber.android.data.extension.otr.OTRManager; @@ -306,5 +307,4 @@ protected void onComplete() { super.onComplete(); sendMessages(); } - } diff --git a/xabber/src/main/java/com/xabber/android/data/message/chat/ChatManager.java b/xabber/src/main/java/com/xabber/android/data/message/chat/ChatManager.java index 2ac2704037..c7111125ca 100644 --- a/xabber/src/main/java/com/xabber/android/data/message/chat/ChatManager.java +++ b/xabber/src/main/java/com/xabber/android/data/message/chat/ChatManager.java @@ -510,7 +510,6 @@ public void execute(Realm realm) { chatRealm = new ChatDataRealm(accountJid, userJid); chatRealm.setLastPosition(chat.getLastPosition()); - chatRealm.setUnreadCount(chat.getUnreadMessageCount()); chatRealm.setArchived(chat.isArchived()); NotificationStateRealm notificationStateRealm = chatRealm.getNotificationState(); @@ -556,7 +555,6 @@ public ChatData loadChatDataFromRealm(AbstractChat chat) { realmChat.getSubject(), realmChat.getAccountJid(), realmChat.getUserJid(), - realmChat.getUnreadCount(), realmChat.isArchived(), notificationState, realmChat.getLastPosition()); diff --git a/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationCreator.java b/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationCreator.java index 3061e80706..5a2ebd9c17 100644 --- a/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationCreator.java +++ b/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationCreator.java @@ -19,6 +19,8 @@ import com.xabber.android.R; import com.xabber.android.data.Application; import com.xabber.android.data.SettingsManager; +import com.xabber.android.data.account.AccountItem; +import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.entity.AccountJid; import com.xabber.android.data.entity.UserJid; import com.xabber.android.data.extension.avatar.AvatarManager; @@ -71,7 +73,7 @@ public void createNotification(MessageNotificationManager.Chat chat, boolean ale .setContentIntent(createContentIntent(chat)) .setDeleteIntent(NotificationReceiver.createDeleteIntent(context, chat.getNotificationId())) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setPriority(inForeground ? NotificationCompat.PRIORITY_DEFAULT + .setPriority((inForeground || inGracePeriod(chat)) ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_HIGH); boolean showText = isNeedShowTextInNotification(chat); @@ -85,7 +87,7 @@ public void createNotification(MessageNotificationManager.Chat chat, boolean ale .setAutoCancel(true); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && alert) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && alert && !inGracePeriod(chat)) addEffects(builder, chat.getLastMessage().getMessageText().toString(), chat, context); builder.addAction(createMarkAsReadAction(chat.getNotificationId())) @@ -109,7 +111,7 @@ public void createBundleNotification(List chats .setContentIntent(createBundleContentIntent()) .setDeleteIntent(NotificationReceiver.createDeleteIntent(context, MESSAGE_BUNDLE_NOTIFICATION_ID)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setPriority(inForeground ? NotificationCompat.PRIORITY_DEFAULT + .setPriority((inForeground || inGracePeriod(lastChat)) ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_HIGH); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -127,7 +129,7 @@ public void createBundleNotification(List chats if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && alert) { MessageNotificationManager.Message lastMessage = lastChat != null ? lastChat.getLastMessage() : null; - if (lastMessage != null) + if (lastMessage != null && alert && !inGracePeriod(lastChat)) addEffects(builder, lastMessage.getMessageText().toString(), lastChat, context); } @@ -135,6 +137,9 @@ public void createBundleNotification(List chats } private String getChannelID(MessageNotificationManager.Chat chat) { + if (inGracePeriod(chat)) + return NotificationChannelUtils.SILENT_CHANNEL_ID; + NotifyPrefs customPrefs = null; boolean isGroup = false; if (chat != null) { @@ -158,6 +163,13 @@ private void sendNotification(NotificationCompat.Builder builder, int notificati } /** UTILS */ + private static boolean inGracePeriod(MessageNotificationManager.Chat chat) { + if (chat == null) return false; + AccountItem accountItem = AccountManager.getInstance().getAccount(chat.getAccountJid()); + if (accountItem != null) return accountItem.inGracePeriod(); + else return false; + } + private CharSequence createNewMessagesTitle(int messageCount) { return context.getString(R.string.new_chat_messages, messageCount, StringUtils.getQuantityString(context.getResources(), R.array.chat_message_quantity, messageCount)); diff --git a/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationManager.java b/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationManager.java index 1c02d8cd46..ed6dedc77c 100644 --- a/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationManager.java +++ b/xabber/src/main/java/com/xabber/android/data/notification/MessageNotificationManager.java @@ -3,10 +3,12 @@ import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import android.os.Handler; import com.xabber.android.R; import com.xabber.android.data.Application; import com.xabber.android.data.OnLoadListener; +import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.database.RealmManager; import com.xabber.android.data.database.messagerealm.Attachment; import com.xabber.android.data.database.messagerealm.MessageItem; @@ -25,8 +27,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.Timer; -import java.util.TimerTask; import java.util.UUID; import io.realm.Realm; @@ -114,7 +114,8 @@ public void onNotificationMarkedAsRead(int notificationId) { AbstractChat chat = MessageManager.getInstance().getChat( chatNotif.getAccountJid(), chatNotif.getUserJid()); if (chat != null) { - chat.resetUnreadMessageCount(); + AccountManager.getInstance().stopGracePeriod(chat.getAccount()); + chat.markAsReadAll(true); callUiUpdate(); } } @@ -420,7 +421,7 @@ public class Chat { private CharSequence chatTitle; private boolean isGroupChat; private List messages = new ArrayList<>(); - private Timer removeTimer; + private Handler removeTimer; public Chat(AccountJid accountJid, UserJid userJid, int notificationId, CharSequence chatTitle, boolean isGroupChat) { @@ -487,23 +488,28 @@ public boolean equals(AccountJid account, UserJid user) { } public void startRemoveTimer() { - stopRemoveTimer(); - removeTimer = new Timer(); - removeTimer.schedule(new TimerTask() { + Application.getInstance().runOnUiThread(new Runnable() { @Override public void run() { - Application.getInstance().runOnUiThread(new Runnable() { + stopRemoveTimer(); + removeTimer = new Handler(); + removeTimer.postDelayed(new Runnable() { @Override public void run() { - removeChat(notificationId); + Application.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + removeChat(notificationId); + } + }); } - }); + }, 500); } - }, 500); + }); } public void stopRemoveTimer() { - if (removeTimer != null) removeTimer.cancel(); + if (removeTimer != null) removeTimer.removeCallbacksAndMessages(null); } } diff --git a/xabber/src/main/java/com/xabber/android/data/notification/NotificationChannelUtils.java b/xabber/src/main/java/com/xabber/android/data/notification/NotificationChannelUtils.java index 5638eea8ce..1ef2be71b5 100644 --- a/xabber/src/main/java/com/xabber/android/data/notification/NotificationChannelUtils.java +++ b/xabber/src/main/java/com/xabber/android/data/notification/NotificationChannelUtils.java @@ -27,6 +27,7 @@ public class NotificationChannelUtils { public static final String DEFAULT_ATTENTION_CHANNEL_ID = "DEFAULT_ATTENTION_CHANNEL_ID"; public static final String PERSISTENT_CONNECTION_CHANNEL_ID = "PERSISTENT_CONNECTION_CHANNEL_ID"; public static final String EVENTS_CHANNEL_ID = "EVENTS_CHANNEL_ID"; + public static final String SILENT_CHANNEL_ID = "SILENT_CHANNEL_ID"; public enum ChannelType { privateChat, @@ -133,6 +134,18 @@ public static String createEventsChannel(NotificationManager notifManager) { return channel.getId(); } + @RequiresApi(api = Build.VERSION_CODES.O) + public static String createSilentChannel(NotificationManager notifManager) { + @SuppressLint("WrongConstant") NotificationChannel channel = + new NotificationChannel(SILENT_CHANNEL_ID, + getString(R.string.channel_silent_title), + NotificationManager.IMPORTANCE_LOW); + channel.setDescription(getString(R.string.channel_silent_description)); + channel.setShowBadge(true); + notifManager.createNotificationChannel(channel); + return channel.getId(); + } + private static String getString(@StringRes int resid) { return Application.getInstance().getString(resid); } diff --git a/xabber/src/main/java/com/xabber/android/data/notification/NotificationManager.java b/xabber/src/main/java/com/xabber/android/data/notification/NotificationManager.java index 4e4dc1d509..da804d1cb5 100644 --- a/xabber/src/main/java/com/xabber/android/data/notification/NotificationManager.java +++ b/xabber/src/main/java/com/xabber/android/data/notification/NotificationManager.java @@ -108,6 +108,7 @@ private NotificationManager() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager != null) { NotificationChannelUtils.createPresistentConnectionChannel(notificationManager); NotificationChannelUtils.createEventsChannel(notificationManager); + NotificationChannelUtils.createSilentChannel(notificationManager); } handler = new Handler(); diff --git a/xabber/src/main/java/com/xabber/android/data/roster/PresenceManager.java b/xabber/src/main/java/com/xabber/android/data/roster/PresenceManager.java index 66b7493e82..bec231daff 100644 --- a/xabber/src/main/java/com/xabber/android/data/roster/PresenceManager.java +++ b/xabber/src/main/java/com/xabber/android/data/roster/PresenceManager.java @@ -116,6 +116,15 @@ public SubscriptionRequest getSubscriptionRequest(AccountJid account, UserJid us * @throws NetworkException */ public void requestSubscription(AccountJid account, UserJid user) throws NetworkException { + requestSubscription(account, user, true); + } + + /** + * Requests subscription to the contact. + * Create chat with new contact if need. + * @throws NetworkException + */ + public void requestSubscription(AccountJid account, UserJid user, boolean createChat) throws NetworkException { Presence packet = new Presence(Presence.Type.subscribe); packet.setTo(user.getJid()); StanzaSender.sendStanza(account, packet); @@ -125,7 +134,7 @@ public void requestSubscription(AccountJid account, UserJid user) throws Network requestedSubscriptions.put(account, set); } set.add(user); - createChatForNewContact(account, user); + if (createChat) createChatForNewContact(account, user); } private void removeRequestedSubscription(AccountJid account, UserJid user) { diff --git a/xabber/src/main/java/com/xabber/android/data/xaccount/XMPPAuthManager.java b/xabber/src/main/java/com/xabber/android/data/xaccount/XMPPAuthManager.java index cccaa4f962..3be199ae28 100644 --- a/xabber/src/main/java/com/xabber/android/data/xaccount/XMPPAuthManager.java +++ b/xabber/src/main/java/com/xabber/android/data/xaccount/XMPPAuthManager.java @@ -179,7 +179,7 @@ private void addContactToRoster(String apiJid, String clientJid) { RosterManager.getInstance().createContact(account, user, "xabber", Collections.EMPTY_LIST); - PresenceManager.getInstance().requestSubscription(account, user); + PresenceManager.getInstance().requestSubscription(account, user, false); } catch (UserJid.UserJidCreateException | XmppStringprepException | InterruptedException | SmackException | NetworkException | XMPPException.XMPPErrorException e) { diff --git a/xabber/src/main/java/com/xabber/android/presentation/mvp/contactlist/ContactListPresenter.java b/xabber/src/main/java/com/xabber/android/presentation/mvp/contactlist/ContactListPresenter.java index d831703b19..c2dfe3e619 100644 --- a/xabber/src/main/java/com/xabber/android/presentation/mvp/contactlist/ContactListPresenter.java +++ b/xabber/src/main/java/com/xabber/android/presentation/mvp/contactlist/ContactListPresenter.java @@ -23,6 +23,7 @@ import com.xabber.android.data.message.ChatContact; import com.xabber.android.data.message.CrowdfundingChat; import com.xabber.android.data.message.MessageManager; +import com.xabber.android.data.message.MessageUpdateEvent; import com.xabber.android.data.message.NewMessageEvent; import com.xabber.android.data.roster.AbstractContact; import com.xabber.android.data.roster.CrowdfundingContact; @@ -51,7 +52,6 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; -import org.jxmpp.stringprep.XmppStringprepException; import java.util.ArrayList; import java.util.Collection; @@ -209,6 +209,11 @@ public void onNewMessageEvent(NewMessageEvent event) { updateBackpressure.refreshRequest(); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEvent(MessageUpdateEvent event) { + updateBackpressure.refreshRequest(); + } + @Override public void update() { // listener.hidePlaceholder(); @@ -452,6 +457,7 @@ public void update() { } else view.hidePlaceholder(); view.updateItems(items); } + updateUnreadCount(); } /** @@ -465,19 +471,17 @@ private GroupConfiguration getChatsGroup(Collection chats, ChatLis List newChats = new ArrayList<>(); - int unreadMessageCount = 0; for (AbstractChat abstractChat : chats) { MessageItem lastMessage = abstractChat.getLastMessage(); if (lastMessage != null) { AccountItem accountItem = AccountManager.getInstance().getAccount(abstractChat.getAccount()); if (accountItem != null && accountItem.isEnabled()) { - int unread = abstractChat.getUnreadMessageCount(); - if (abstractChat.notifyAboutMessage()) unreadMessageCount = unreadMessageCount + unread; switch (state) { case unread: - if (!abstractChat.isArchived() && unread > 0) newChats.add(abstractChat); + if (!abstractChat.isArchived() && abstractChat.getUnreadMessageCount() > 0) + newChats.add(abstractChat); break; case archived: if (abstractChat.isArchived()) newChats.add(abstractChat); @@ -508,9 +512,7 @@ private GroupConfiguration getChatsGroup(Collection chats, ChatLis break; } } - unreadMessageCount += unreadCount; - EventBus.getDefault().post(new UpdateUnreadCountEvent(unreadMessageCount)); Collections.sort(newChats, ChatComparator.CHAT_COMPARATOR); chatsGroup.setNotEmpty(); @@ -652,6 +654,18 @@ public ArrayList getTwoNextRecentChat() { return items; } + public void updateUnreadCount() { + int unreadMessageCount = 0; + + for (AbstractChat abstractChat : MessageManager.getInstance().getChatsOfEnabledAccount()) { + if (abstractChat.notifyAboutMessage() && !abstractChat.isArchived()) + unreadMessageCount += abstractChat.getUnreadMessageCount(); + } + + unreadMessageCount += CrowdfundingManager.getInstance().getUnreadMessageCount(); + EventBus.getDefault().post(new UpdateUnreadCountEvent(unreadMessageCount)); + } + public enum ChatListState { recent, unread, diff --git a/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/ContactListFragment.java b/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/ContactListFragment.java index d9603df4fa..5381f7883e 100644 --- a/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/ContactListFragment.java +++ b/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/ContactListFragment.java @@ -268,6 +268,9 @@ public void onItemSwipe(int position, int direction) { // showing snackbar with Undo option showSnackbar(deletedItem, position); + + // update unread count + presenter.updateUnreadCount(); } } @@ -381,6 +384,9 @@ public void onClick(View view) { // undo is selected, restore the deleted item adapter.addItem(deletedIndex, deletedItem); + + // update unread count + presenter.updateUnreadCount(); } }); snackbar.setActionTextColor(Color.YELLOW); diff --git a/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ContactVO.java b/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ContactVO.java index 4304b79ad8..65ffddbf6d 100644 --- a/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ContactVO.java +++ b/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ContactVO.java @@ -32,6 +32,7 @@ import com.xabber.android.data.extension.muc.RoomChat; import com.xabber.android.data.filedownload.FileCategory; import com.xabber.android.data.message.AbstractChat; +import com.xabber.android.data.message.ChatAction; import com.xabber.android.data.message.MessageManager; import com.xabber.android.data.message.NotificationState; import com.xabber.android.data.notification.custom_notification.CustomNotifyPrefsManager; @@ -184,6 +185,8 @@ public static ContactVO convert(AbstractContact contact, ContactClickListener li messageText = FileCategory.getCategoryName(category, true) + attachment.getTitle(); } else if (lastMessage.getFilePath() != null) { messageText = new File(lastMessage.getFilePath()).getName(); + } else if (ChatAction.available.toString().equals(lastMessage.getAction())) { + messageText = "" + lastMessage.getText().trim() + ""; } else { messageText = lastMessage.getText().trim(); } @@ -197,18 +200,17 @@ public static ContactVO convert(AbstractContact contact, ContactClickListener li messageOwner = lastMessage.getResource().toString(); // message status - if (lastMessage.isForwarded()) { + if (lastMessage.isError()) { + messageStatus = 4; + } else if (!MessageItem.isUploadFileMessage(lastMessage) && !lastMessage.isSent() + && System.currentTimeMillis() - lastMessage.getTimestamp() > 1000) { + messageStatus = 5; + } else if (lastMessage.isDisplayed() || lastMessage.isReceivedFromMessageArchive()) { messageStatus = 1; - } else if (lastMessage.isReceivedFromMessageArchive()) { + } else if (lastMessage.isDelivered() || lastMessage.isForwarded()) { messageStatus = 2; - } else if (lastMessage.isError()) { + } else if (lastMessage.isAcknowledged()) { messageStatus = 3; - } else if (!lastMessage.isDelivered()) { - if (lastMessage.isAcknowledged()) { - messageStatus = 4; - } else { - messageStatus = 5; - } } // forwarded diff --git a/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ExtContactVO.java b/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ExtContactVO.java index 47ddc972ac..f7a79371ca 100644 --- a/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ExtContactVO.java +++ b/xabber/src/main/java/com/xabber/android/presentation/ui/contactlist/viewobjects/ExtContactVO.java @@ -110,14 +110,18 @@ public void bindViewHolder(FlexibleAdapter adapter, ViewHolder viewHolder, int p viewHolder.ivMessageStatus.setVisibility(text.isEmpty() ? View.INVISIBLE : View.VISIBLE); switch (getMessageStatus()) { - case 0: + + case 1: + viewHolder.ivMessageStatus.setImageResource(R.drawable.ic_message_displayed); + break; + case 2: viewHolder.ivMessageStatus.setImageResource(R.drawable.ic_message_delivered_14dp); break; case 3: - viewHolder.ivMessageStatus.setImageResource(R.drawable.ic_message_has_error_14dp); + viewHolder.ivMessageStatus.setImageResource(R.drawable.ic_message_acknowledged_14dp); break; case 4: - viewHolder.ivMessageStatus.setImageResource(R.drawable.ic_message_acknowledged_14dp); + viewHolder.ivMessageStatus.setImageResource(R.drawable.ic_message_has_error_14dp); break; default: viewHolder.ivMessageStatus.setVisibility(View.GONE); diff --git a/xabber/src/main/java/com/xabber/android/service/UploadService.java b/xabber/src/main/java/com/xabber/android/service/UploadService.java index 289a0c5ab2..ebee560278 100644 --- a/xabber/src/main/java/com/xabber/android/service/UploadService.java +++ b/xabber/src/main/java/com/xabber/android/service/UploadService.java @@ -17,6 +17,7 @@ import com.xabber.android.data.entity.UserJid; import com.xabber.android.data.extension.file.FileManager; import com.xabber.android.data.extension.file.FileUtils; +import com.xabber.android.data.extension.file.UriUtils; import com.xabber.android.data.extension.httpfileupload.ImageCompressor; import com.xabber.android.data.log.LogManager; import com.xabber.android.data.message.MessageManager; @@ -115,7 +116,10 @@ private void startWorkWithUris(AccountJid account, UserJid user, List fileU } // create message with progress - String messageId = MessageManager.getInstance().createFileMessage(account, user, files); + String messageId; + if (remoteFiles.isEmpty()) + messageId = MessageManager.getInstance().createFileMessage(account, user, files); + else messageId = MessageManager.getInstance().createFileMessageFromUris(account, user, remoteFiles); // create dir File directory = new File(getDownloadDirPath()); @@ -127,30 +131,31 @@ private void startWorkWithUris(AccountJid account, UserJid user, List fileU // get files from uri's List copiedFiles = new ArrayList<>(); - for (Uri uri : remoteFiles) { - if (needStop) { - stopWork(messageId); - return; - } + if (!remoteFiles.isEmpty()) { + for (Uri uri : remoteFiles) { + if (needStop) { + stopWork(messageId); + return; + } - // copy file to local storage if need - try { - copiedFiles.add(new File(copyFileToLocalStorage(uri))); - } catch (IOException e) { - publishError(messageId, "Cannot get file: " + e.toString()); + // copy file to local storage if need + try { + copiedFiles.add(new File(copyFileToLocalStorage(uri))); + } catch (IOException e) { + publishError(messageId, "Cannot get file: " + e.toString()); + } + publishProgress(messageId, copiedFiles.size(), remoteFiles.size()); } - publishProgress(messageId, copiedFiles.size(), remoteFiles.size()); } // add attachments to message - MessageManager.getInstance().updateMessageWithNewAttachments(messageId, copiedFiles); + files.addAll(copiedFiles); + MessageManager.getInstance().updateMessageWithNewAttachments(messageId, files); // startWork for upload files List filePaths = new ArrayList<>(); for (File file : files) filePaths.add(file.getPath()); - for (File file : copiedFiles) - filePaths.add(file.getPath()); startWork(account, user, filePaths, uploadServerUrl, messageId); } @@ -341,11 +346,7 @@ private String generateErrorDescriptionForFiles(List files, List } private String copyFileToLocalStorage(Uri uri) throws IOException { - String extension = getExtensionFromUri(uri); - String name = getFileName(uri); - if (name == null) name = UUID.randomUUID().toString(); - else name = name.replace(".", ""); - String fileName = name + "." + extension; + String fileName = UriUtils.getFullFileName(uri); File file = new File(getDownloadDirPath(), fileName); OutputStream os = null; @@ -371,31 +372,4 @@ private String copyFileToLocalStorage(Uri uri) throws IOException { } return file.getPath(); } - - private String getExtensionFromUri(Uri uri) { - String mimeType = getContentResolver().getType(uri); - return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); - } - - private String getFileName(Uri uri) { - String result = null; - if (uri.getScheme().equals("content")) { - Cursor cursor = getContentResolver().query(uri, null, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); - } - } finally { - cursor.close(); - } - } - if (result == null) { - result = uri.getPath(); - int cut = result.lastIndexOf('/'); - if (cut != -1) { - result = result.substring(cut + 1); - } - } - return FilenameUtils.getBaseName(result); - } } diff --git a/xabber/src/main/java/com/xabber/android/ui/activity/ChatActivity.java b/xabber/src/main/java/com/xabber/android/ui/activity/ChatActivity.java index 888ec7656b..7c8e5c9906 100644 --- a/xabber/src/main/java/com/xabber/android/ui/activity/ChatActivity.java +++ b/xabber/src/main/java/com/xabber/android/ui/activity/ChatActivity.java @@ -52,7 +52,6 @@ import com.xabber.android.data.extension.attention.AttentionManager; import com.xabber.android.data.extension.blocking.BlockingManager; import com.xabber.android.data.extension.blocking.OnBlockedListChangedListener; -import com.xabber.android.data.extension.file.FileUtils; import com.xabber.android.data.extension.httpfileupload.HttpFileUploadManager; import com.xabber.android.data.extension.muc.MUCManager; import com.xabber.android.data.extension.muc.RoomChat; @@ -62,6 +61,7 @@ import com.xabber.android.data.message.AbstractChat; import com.xabber.android.data.message.CrowdfundingChat; import com.xabber.android.data.message.MessageManager; +import com.xabber.android.data.message.MessageUpdateEvent; import com.xabber.android.data.message.NewMessageEvent; import com.xabber.android.data.message.NotificationState; import com.xabber.android.data.message.RegularChat; @@ -280,6 +280,14 @@ public static Intent createSendUriIntent(Context context, AccountJid account, return intent; } + public static Intent createSendUrisIntent(Context context, AccountJid account, + UserJid user, ArrayList uris) { + Intent intent = ChatActivity.createSpecificChatIntent(context, account, user); + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + return intent; + } + public static Intent createAttentionRequestIntent(Context context, AccountJid account, UserJid user) { Intent intent = ChatActivity.createClearTopIntent(context, account, user); intent.setAction(ACTION_ATTENTION); @@ -372,15 +380,23 @@ protected void onResume() { && intent.getParcelableExtra(Intent.EXTRA_STREAM) != null) { Uri receivedUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + intent.removeExtra(Intent.EXTRA_STREAM); handleShareFileUri(receivedUri); } else if (Intent.ACTION_SEND.equals(intent.getAction())) { - extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + extraText = intent.getStringExtra(Intent.EXTRA_TEXT); if (extraText != null) { intent.removeExtra(Intent.EXTRA_TEXT); exitOnSend = true; } + + } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { + ArrayList uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + if (uris != null) { + intent.removeExtra(Intent.EXTRA_STREAM); + handleShareFileUris(uris); + } } needScrollToUnread = intent.getBooleanExtra(EXTRA_NEED_SCROLL_TO_UNREAD, false); @@ -404,19 +420,26 @@ protected void onResume() { } public void handleShareFileUri(Uri fileUri) { - final String path = FileUtils.getPath(this, fileUri); - - LogManager.i(this, String.format("File uri: %s, path: %s", fileUri, path)); + if (PermissionsRequester.requestFileReadPermissionIfNeeded(this, PERMISSIONS_REQUEST_ATTACH_FILE)) { + List uris = new ArrayList<>(); + uris.add(fileUri); + HttpFileUploadManager.getInstance().uploadFileViaUri(account, user, uris, this); + } + } - if (path == null) { + public void handleShareFileUris(ArrayList uris) { + if (uris.size() == 0) { Toast.makeText(this, R.string.could_not_get_path_to_file, Toast.LENGTH_SHORT).show(); return; } + if (uris.size() > 10) { + Toast.makeText(this, R.string.too_many_files_at_once, Toast.LENGTH_SHORT).show(); + return; + } + if (PermissionsRequester.requestFileReadPermissionIfNeeded(this, PERMISSIONS_REQUEST_ATTACH_FILE)) { - List paths = new ArrayList<>(); - paths.add(path); - HttpFileUploadManager.getInstance().uploadFile(account, user, paths, this); + HttpFileUploadManager.getInstance().uploadFileViaUri(account, user, uris, this); } } @@ -499,6 +522,9 @@ private void getSelectedPageDataFromIntent() { case Intent.ACTION_SEND: selectedPagePosition = ChatViewerAdapter.PAGE_POSITION_CHAT; break; + case Intent.ACTION_SEND_MULTIPLE: + selectedPagePosition = ChatViewerAdapter.PAGE_POSITION_CHAT; + break; case ACTION_FORWARD: selectedPagePosition = ChatViewerAdapter.PAGE_POSITION_CHAT; break; @@ -579,6 +605,11 @@ public void onNewMessageEvent(NewMessageEvent event) { updateBackpressure.refreshRequest(); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEvent(MessageUpdateEvent event) { + updateBackpressure.refreshRequest(); + } + @Override public void onContactsChanged(Collection entities) { updateBackpressure.refreshRequest(); diff --git a/xabber/src/main/java/com/xabber/android/ui/activity/ContactAddActivity.java b/xabber/src/main/java/com/xabber/android/ui/activity/ContactAddActivity.java index 4f20a70580..3d8a4b425f 100644 --- a/xabber/src/main/java/com/xabber/android/ui/activity/ContactAddActivity.java +++ b/xabber/src/main/java/com/xabber/android/ui/activity/ContactAddActivity.java @@ -21,6 +21,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; import com.xabber.android.R; import com.xabber.android.data.entity.AccountJid; @@ -33,7 +35,9 @@ public class ContactAddActivity extends ManagedActivity implements ContactAddFragment.Listener { - BarPainter barPainter; + private BarPainter barPainter; + private ProgressBar progressBar; + private Toolbar toolbar; public static Intent createIntent(Context context) { return createIntent(context, null); @@ -59,9 +63,9 @@ private static UserJid getUser(Intent intent) { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_with_toolbar_and_container); + setContentView(R.layout.activity_with_toolbar_progress_and_container); - Toolbar toolbar = ToolbarHelper.setUpDefaultToolbar(this, null, R.drawable.ic_clear_white_24dp); + toolbar = ToolbarHelper.setUpDefaultToolbar(this, null, R.drawable.ic_clear_white_24dp); toolbar.inflateMenu(R.menu.toolbar_add_contact); toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { @Override @@ -70,6 +74,7 @@ public boolean onMenuItemClick(MenuItem item) { } }); + progressBar = findViewById(R.id.toolbarProgress); barPainter = new BarPainter(this, toolbar); barPainter.setDefaultColor(); @@ -112,4 +117,12 @@ public boolean onOptionsItemSelected(MenuItem item) { public void onAccountSelected(AccountJid account) { barPainter.updateWithAccountName(account); } + + @Override + public void showProgress(boolean show) { + if (progressBar != null) { + progressBar.setVisibility(show ? View.VISIBLE : View.GONE); + toolbar.getMenu().findItem(R.id.action_add_contact).setVisible(!show); + } + } } diff --git a/xabber/src/main/java/com/xabber/android/ui/activity/ContactListActivity.java b/xabber/src/main/java/com/xabber/android/ui/activity/ContactListActivity.java index 1e54e709cf..f3e5ed6700 100644 --- a/xabber/src/main/java/com/xabber/android/ui/activity/ContactListActivity.java +++ b/xabber/src/main/java/com/xabber/android/ui/activity/ContactListActivity.java @@ -192,6 +192,7 @@ private static UserJid getRoomInviteUser(Intent intent) { public void onCreate(Bundle savedInstanceState) { if (Intent.ACTION_VIEW.equals(getIntent().getAction()) || Intent.ACTION_SEND.equals(getIntent().getAction()) + || Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction()) || Intent.ACTION_SENDTO.equals(getIntent().getAction()) || Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) { ActivityManager.getInstance().startNewTask(this); @@ -342,6 +343,8 @@ protected void onResume() { switch (action) { case ContactListActivity.ACTION_ROOM_INVITE: case Intent.ACTION_SEND: + case Intent.ACTION_SEND_MULTIPLE: + case ChatActivity.ACTION_FORWARD: case Intent.ACTION_CREATE_SHORTCUT: if (Intent.ACTION_SEND.equals(action)) { sendText = getIntent().getStringExtra(Intent.EXTRA_TEXT); @@ -629,6 +632,15 @@ public void onContactClick(AbstractContact abstractContact) { finish(); } break; + case Intent.ACTION_SEND_MULTIPLE: + if (getIntent().getExtras() != null) { + action = null; + startActivity(ChatActivity.createSendUrisIntent(this, + abstractContact.getAccount(), abstractContact.getUser(), + getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM))); + finish(); + } + break; case Intent.ACTION_CREATE_SHORTCUT: { createShortcut(abstractContact); finish(); diff --git a/xabber/src/main/java/com/xabber/android/ui/adapter/CustomMessageMenuAdapter.java b/xabber/src/main/java/com/xabber/android/ui/adapter/CustomMessageMenuAdapter.java index 8160b0b6fc..a3045a75d3 100644 --- a/xabber/src/main/java/com/xabber/android/ui/adapter/CustomMessageMenuAdapter.java +++ b/xabber/src/main/java/com/xabber/android/ui/adapter/CustomMessageMenuAdapter.java @@ -22,6 +22,7 @@ public class CustomMessageMenuAdapter extends BaseAdapter { public final static String STATUS_ACK = "status_acknowledged"; public final static String STATUS_DELIVERED = "status_delivered"; + public final static String STATUS_DISPLAYED = "status_displayed"; public final static String STATUS_ERROR = "status_error"; public final static String STATUS_FORWARDED = "status_forwarded"; public final static String STATUS_SYNCED = "status_synced"; @@ -75,6 +76,10 @@ public View getView(int position, View convertView, ViewGroup parent) { textView.setText(R.string.message_status_delivered); ivStatus.setImageResource(R.drawable.ic_message_delivered_14dp); break; + case STATUS_DISPLAYED: + textView.setText(R.string.message_status_displayed); + ivStatus.setImageResource(R.drawable.ic_message_displayed); + break; case STATUS_SYNCED: textView.setText(R.string.message_status_synced); ivStatus.setImageResource(R.drawable.ic_message_synced_14dp); diff --git a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/IncomingMessageVH.java b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/IncomingMessageVH.java index 94f31b43c0..cf9724bfce 100644 --- a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/IncomingMessageVH.java +++ b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/IncomingMessageVH.java @@ -25,23 +25,30 @@ public class IncomingMessageVH extends FileMessageVH { public ImageView avatar; public ImageView avatarBackground; + private BindListener listener; + + public interface BindListener { + void onBind(MessageItem message); + } IncomingMessageVH(View itemView, MessageClickListener messageListener, MessageLongClickListener longClickListener, - FileListener fileListener, @StyleRes int appearance) { + FileListener fileListener, BindListener listener, @StyleRes int appearance) { super(itemView, messageListener, longClickListener, fileListener, appearance); avatar = itemView.findViewById(R.id.avatar); avatarBackground = itemView.findViewById(R.id.avatarBackground); + this.listener = listener; } - public void bind(MessageItem messageItem, MessagesAdapter.MessageExtraData extraData) { + public void bind(final MessageItem messageItem, MessagesAdapter.MessageExtraData extraData) { super.bind(messageItem, extraData); Context context = extraData.getContext(); boolean needTail = extraData.isNeedTail(); // setup ARCHIVED icon - statusIcon.setVisibility(messageItem.isReceivedFromMessageArchive() ? View.VISIBLE : View.GONE); + //statusIcon.setVisibility(messageItem.isReceivedFromMessageArchive() ? View.VISIBLE : View.GONE); + statusIcon.setVisibility(View.GONE); // setup FORWARDED boolean haveForwarded = messageItem.haveForwardedMessages(); @@ -105,6 +112,18 @@ public void bind(MessageItem messageItem, MessagesAdapter.MessageExtraData extra messageBalloon.setVisibility(View.VISIBLE); messageTime.setVisibility(View.VISIBLE); } + + itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + if (listener != null) listener.onBind(messageItem); + } + + @Override + public void onViewDetachedFromWindow(View v) { + unsubscribeAll(); + } + }); } private void setUpAvatar(MessageItem messageItem, boolean isMUC, String userName, boolean needTail) { diff --git a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/MessagesAdapter.java b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/MessagesAdapter.java index 842fb01ac2..19c875d5ce 100644 --- a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/MessagesAdapter.java +++ b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/MessagesAdapter.java @@ -47,6 +47,7 @@ public class MessagesAdapter extends RealmRecyclerViewAdapter itemsNeedOriginalText = new ArrayList<>(); @@ -78,7 +79,7 @@ public MessagesAdapter( Context context, RealmResults messageItems, AbstractChat chat, MessageVH.MessageClickListener messageListener, FileMessageVH.FileListener fileListener, ForwardedAdapter.ForwardListener fwdListener, - Listener listener, AnchorHolder anchorHolder) { + Listener listener, IncomingMessageVH.BindListener bindListener, AnchorHolder anchorHolder) { super(context, messageItems, true); this.context = context; @@ -87,6 +88,7 @@ public MessagesAdapter( this.fwdListener = fwdListener; this.listener = listener; this.anchorHolder = anchorHolder; + this.bindListener = bindListener; account = chat.getAccount(); user = chat.getUser(); @@ -135,12 +137,12 @@ public BasicMessageVH onCreateViewHolder(ViewGroup parent, int viewType) { case VIEW_TYPE_INCOMING_MESSAGE: return new IncomingMessageVH(LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_message_incoming, parent, false), - this, this, this, appearanceStyle); + this, this, this, bindListener, appearanceStyle); case VIEW_TYPE_INCOMING_MESSAGE_NOFLEX: return new NoFlexIncomingMsgVH(LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_message_incoming_noflex, parent, false), - this, this, this, appearanceStyle); + this, this, this, bindListener, appearanceStyle); case VIEW_TYPE_OUTGOING_MESSAGE: return new OutgoingMessageVH(LayoutInflater.from(parent.getContext()) @@ -173,7 +175,7 @@ public void onBindViewHolder(final BasicMessageVH holder, int position) { ((MessageVH)holder).messageId = messageItem.getUniqueId(); // setup message as unread - boolean unread = position == getItemCount() - unreadCount; + boolean unread = messageItem.getUniqueId().equals(firstUnreadMessageID); // setup message as checked boolean checked = checkedItemIds.contains(messageItem.getUniqueId()); @@ -265,15 +267,8 @@ public int findMessagePosition(String uniqueId) { return RecyclerView.NO_POSITION; } - public boolean setUnreadCount(int unreadCount) { - if (this.unreadCount != unreadCount) { - this.unreadCount = unreadCount; - return true; - } else return false; - } - - public int getUnreadCount() { - return unreadCount; + public void setFirstUnreadMessageId(String id) { + firstUnreadMessageID = id; } public void addOrRemoveItemNeedOriginalText(String messageId) { diff --git a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/NoFlexIncomingMsgVH.java b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/NoFlexIncomingMsgVH.java index 8723e45230..e14c120aa6 100644 --- a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/NoFlexIncomingMsgVH.java +++ b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/NoFlexIncomingMsgVH.java @@ -8,8 +8,8 @@ public class NoFlexIncomingMsgVH extends IncomingMessageVH { public NoFlexIncomingMsgVH(View itemView, MessageClickListener messageListener, MessageLongClickListener longClickListener, - FileListener fileListener, int appearance) { - super(itemView, messageListener, longClickListener, fileListener, appearance); + FileListener fileListener, BindListener listener, int appearance) { + super(itemView, messageListener, longClickListener, fileListener, listener, appearance); } @Override diff --git a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/OutgoingMessageVH.java b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/OutgoingMessageVH.java index b5aa1678cb..12dcdde180 100644 --- a/xabber/src/main/java/com/xabber/android/ui/adapter/chat/OutgoingMessageVH.java +++ b/xabber/src/main/java/com/xabber/android/ui/adapter/chat/OutgoingMessageVH.java @@ -105,21 +105,22 @@ private void setStatusIcon(MessageItem messageItem) { if (isFileUploadInProgress) progressBar.setVisibility(View.VISIBLE); - int messageIcon = R.drawable.ic_message_delivered_14dp; - if (messageItem.isForwarded()) { - messageIcon = R.drawable.ic_message_forwarded_14dp; - } else if (messageItem.isReceivedFromMessageArchive()) { - messageIcon = R.drawable.ic_message_synced_14dp; - } else if (messageItem.isError()) { + int messageIcon = 0; + + if (messageItem.isError()) { messageIcon = R.drawable.ic_message_has_error_14dp; } else if (!isFileUploadInProgress && !messageItem.isSent() && System.currentTimeMillis() - messageItem.getTimestamp() > 1000) { messageIcon = R.drawable.ic_message_not_sent_14dp; - } else if (!messageItem.isDelivered()) { - if (messageItem.isAcknowledged()) - messageIcon = R.drawable.ic_message_acknowledged_14dp; - else statusIcon.setVisibility(View.GONE); + } else if (messageItem.isDisplayed() || messageItem.isReceivedFromMessageArchive()) { + messageIcon = R.drawable.ic_message_displayed; + } else if (messageItem.isDelivered() || messageItem.isForwarded()) { + messageIcon = R.drawable.ic_message_delivered_14dp; + } else if (messageItem.isAcknowledged()) { + messageIcon = R.drawable.ic_message_acknowledged_14dp; } - statusIcon.setImageResource(messageIcon); + + if (messageIcon != 0) statusIcon.setImageResource(messageIcon); + else statusIcon.setVisibility(View.INVISIBLE); } } diff --git a/xabber/src/main/java/com/xabber/android/ui/fragment/AccountAddFragment.java b/xabber/src/main/java/com/xabber/android/ui/fragment/AccountAddFragment.java index 3322b697b4..796ec08880 100644 --- a/xabber/src/main/java/com/xabber/android/ui/fragment/AccountAddFragment.java +++ b/xabber/src/main/java/com/xabber/android/ui/fragment/AccountAddFragment.java @@ -97,7 +97,7 @@ public void addAccount() { AccountJid account; try { account = AccountManager.getInstance().addAccount( - userView.getText().toString().trim(), + userView.getText().toString().replace(" ", ""), passwordView.getText().toString(), "", false, diff --git a/xabber/src/main/java/com/xabber/android/ui/fragment/ChatFragment.java b/xabber/src/main/java/com/xabber/android/ui/fragment/ChatFragment.java index 3237d7d02e..27f3ed01e8 100644 --- a/xabber/src/main/java/com/xabber/android/ui/fragment/ChatFragment.java +++ b/xabber/src/main/java/com/xabber/android/ui/fragment/ChatFragment.java @@ -76,6 +76,7 @@ import com.xabber.android.data.message.MessageManager; import com.xabber.android.data.message.MessageUpdateEvent; import com.xabber.android.data.message.NewIncomingMessageEvent; +import com.xabber.android.data.message.NewMessageEvent; import com.xabber.android.data.message.RegularChat; import com.xabber.android.data.message.chat.ChatManager; import com.xabber.android.data.notification.NotificationManager; @@ -86,6 +87,7 @@ import com.xabber.android.ui.activity.QuestionActivity; import com.xabber.android.ui.adapter.CustomMessageMenuAdapter; import com.xabber.android.ui.adapter.ResourceAdapter; +import com.xabber.android.ui.adapter.chat.IncomingMessageVH; import com.xabber.android.ui.adapter.chat.MessageVH; import com.xabber.android.ui.adapter.chat.MessagesAdapter; import com.xabber.android.ui.color.ColorManager; @@ -122,7 +124,8 @@ public class ChatFragment extends FileInteractionFragment implements PopupMenu.OnMenuItemClickListener, View.OnClickListener, Toolbar.OnMenuItemClickListener, MessageVH.MessageClickListener, MessagesAdapter.Listener, AdapterView.OnItemClickListener, PopupWindow.OnDismissListener, - OnAccountChangedListener, ForwardPanel.OnCloseListener, MessagesAdapter.AnchorHolder { + OnAccountChangedListener, ForwardPanel.OnCloseListener, MessagesAdapter.AnchorHolder, + IncomingMessageVH.BindListener { public static final String ARGUMENT_ACCOUNT = "ARGUMENT_ACCOUNT"; public static final String ARGUMENT_USER = "ARGUMENT_USER"; @@ -339,7 +342,6 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { } showScrollDownButtonIfNeed(); - hideUnreadMessageCountIfNeed(); /** Necessary for * @see MessageVH#bind () @@ -399,7 +401,8 @@ public void setChat(AccountJid accountJid, UserJid userJid) { } chatMessageAdapter = new MessagesAdapter(getActivity(), messageItems, abstractChat, - this, this, this, this, this); + this, this, this, this, this, + this); realmRecyclerView.setAdapter(chatMessageAdapter); restoreInputState(); @@ -731,7 +734,10 @@ public void onEvent(PreviousHistoryLoadFinishedEvent event) { @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(MessageUpdateEvent event) { - chatMessageAdapter.onChange(); + if (account.equals(event.getAccount()) && user.equals(event.getUser())) { + updateUnread(); + chatMessageAdapter.onChange(); + } } @Subscribe(threadMode = ThreadMode.MAIN) @@ -739,11 +745,14 @@ public void onEvent(NewIncomingMessageEvent event) { if (event.getAccount().equals(account) && event.getUser().equals(user)) { listener.playIncomingAnimation(); //playIncomingSound(); - increaseUnreadMessageCountIfNeed(); - chatMessageAdapter.setUnreadCount(chatMessageAdapter.getUnreadCount() + 1); } } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEvent(NewMessageEvent event) { + updateUnread(); + } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(AuthAskEvent event) { if (event.getAccount() == getAccount() && event.getUser() == getUser()) { @@ -858,7 +867,7 @@ private void sendMessage() { private void sendMessage(String text) { MessageManager.getInstance().sendMessage(account, user, text); - hideUnreadMessageBackground(); + setFirstUnreadMessageId(null); scrollDown(); } @@ -868,15 +877,27 @@ public void updateContact() { updateSendButtonSecurityLevel(); } + private void onScrollDownClick() { + AbstractChat chat = getChat(); + if (chat != null) { + int unread = chat.getUnreadMessageCount(); + int lastVisiblePosition = layoutManager.findLastVisibleItemPosition(); + if (unread == 0 || lastVisiblePosition + 2 >= chatMessageAdapter.getItemCount() - unread) { + // scroll down + scrollDown(); + + // scroll to unread + } else scrollToFirstUnread(unread); + } + } + private void scrollDown() { - LogManager.i(this, "scrollDown"); realmRecyclerView.scrollToPosition(chatMessageAdapter.getItemCount() - 1); } private void scrollToFirstUnread(int unreadCount) { layoutManager.scrollToPositionWithOffset( chatMessageAdapter.getItemCount() - unreadCount, 200); - showUnreadMessage(unreadCount); } private void updateSecurityButton() { @@ -1072,10 +1093,7 @@ public void onClick(View v) { showJoinButtonIfNeed(); } if (v.getId() == R.id.btnScrollDown) { - AbstractChat chat = getChat(); - if (chat != null && chat.getUnreadMessageCount() > 0) - scrollToFirstUnread(chat.getUnreadMessageCount()); - else scrollDown(); + onScrollDownClick(); } } @@ -1165,14 +1183,12 @@ public void showCustomMenu(View anchor) { CustomMessageMenu.addMenuItem(menuItems, "action_message_show_original_otr", getString(R.string.message_otr_show_original)); } - if (clickedMessageItem.isForwarded()) { - CustomMessageMenu.addMenuItem(menuItems, "action_message_status", CustomMessageMenuAdapter.STATUS_FORWARDED); - } else if (clickedMessageItem.isReceivedFromMessageArchive()) { - CustomMessageMenu.addMenuItem(menuItems, "action_message_status", CustomMessageMenuAdapter.STATUS_SYNCED); - } else if (clickedMessageItem.isError()) { + if (clickedMessageItem.isError()) { CustomMessageMenu.addMenuItem(menuItems, "action_message_status", CustomMessageMenuAdapter.STATUS_ERROR); } else if (!clickedMessageItem.isSent()) { CustomMessageMenu.addMenuItem(menuItems, "action_message_status", CustomMessageMenuAdapter.STATUS_NOT_SEND); + } else if (clickedMessageItem.isDisplayed()) { + CustomMessageMenu.addMenuItem(menuItems, "action_message_status", CustomMessageMenuAdapter.STATUS_DISPLAYED); } else if (clickedMessageItem.isDelivered()) { CustomMessageMenu.addMenuItem(menuItems, "action_message_status", CustomMessageMenuAdapter.STATUS_DELIVERED); } else if (clickedMessageItem.isAcknowledged()) { @@ -1304,18 +1320,15 @@ public void saveScrollState() { public void restoreScrollState(boolean fromNotification) { AbstractChat chat = getChat(); - int position; - int unread; if (chat != null) { - position = chat.getLastPosition(); - unread = chat.getUnreadMessageCount(); + int position = chat.getLastPosition(); + int unread = chat.getUnreadMessageCount(); if ((position == 0 || fromNotification) && unread > 0) scrollToFirstUnread(unread); - else if (position > 0) { + else if (position > 0) layoutManager.scrollToPosition(position); - showUnreadMessage(unread); - updateNewReceivedMessageCounter(unread); - } + setFirstUnreadMessageId(chat.getFirstUnreadMessageId()); + updateNewReceivedMessageCounter(unread); } } @@ -1435,42 +1448,28 @@ private void updateTopDateIfNeed() { tvTopDate.setText(StringUtils.getDateStringForMessage(message.getTimestamp())); } + @Override + public void onBind(MessageItem message) { + if (message != null && message.isValid() && !message.isRead()) { + AbstractChat chat = getChat(); + if (chat != null) chat.markAsRead(message, true); + } + } + + private void updateUnread() { + AbstractChat chat = getChat(); + if (chat != null) updateNewReceivedMessageCounter(chat.getUnreadMessageCount()); + } + private void showScrollDownButtonIfNeed() { int pastVisibleItems = layoutManager.findLastVisibleItemPosition(); boolean isBottom = pastVisibleItems >= chatMessageAdapter.getItemCount() - 1; if (isBottom) { btnScrollDown.setVisibility(View.GONE); - hideUnreadMessageBackground(); } else btnScrollDown.setVisibility(View.VISIBLE); } - private void hideUnreadMessageCountIfNeed() { - AbstractChat chat = getChat(); - if (chat == null) return; - int pastVisibleItems = layoutManager.findLastVisibleItemPosition(); - if (pastVisibleItems >= chatMessageAdapter.getItemCount() - chat.getUnreadMessageCount()) { - resetUnreadMessageCount(); - } - } - - private void increaseUnreadMessageCountIfNeed() { - AbstractChat chat = getChat(); - if (btnScrollDown.getVisibility() == View.VISIBLE && chat != null) { - chat.increaseUnreadMessageCount(); - updateNewReceivedMessageCounter(chat.getUnreadMessageCount()); - } - } - - private void resetUnreadMessageCount() { - AbstractChat chat = getChat(); - if (chat != null) { - chat.resetUnreadMessageCount(); - updateNewReceivedMessageCounter(0); - ((ChatActivity)getActivity()).updateRecentChats(); - } - } - private void updateNewReceivedMessageCounter(int count) { tvNewReceivedCount.setText(String.valueOf(count)); if (count > 0) @@ -1478,22 +1477,11 @@ private void updateNewReceivedMessageCounter(int count) { else tvNewReceivedCount.setVisibility(View.GONE); } - private void showUnreadMessage(int count) { - chatMessageAdapter.setUnreadCount(count); + private void setFirstUnreadMessageId(String id) { + chatMessageAdapter.setFirstUnreadMessageId(id); chatMessageAdapter.notifyDataSetChanged(); } - private void hideUnreadMessageBackground() { - if (chatMessageAdapter.setUnreadCount(0)) { - realmRecyclerView.post(new Runnable() { - @Override - public void run() { - chatMessageAdapter.notifyDataSetChanged(); - } - }); - } - } - private void closeInteractionPanel() { chatMessageAdapter.resetCheckedItems(); setUpInputViewButtons(); @@ -1540,7 +1528,7 @@ private void showForwardPanel(List forwardIds) { private void sendForwardMessage(List messages, String text) { ForwardManager.forwardMessage(messages, account, user, text); hideForwardPanel(); - hideUnreadMessageBackground(); + setFirstUnreadMessageId(null); scrollDown(); } diff --git a/xabber/src/main/java/com/xabber/android/ui/fragment/ContactAddFragment.java b/xabber/src/main/java/com/xabber/android/ui/fragment/ContactAddFragment.java index b8b3157269..2f8a92bb52 100644 --- a/xabber/src/main/java/com/xabber/android/ui/fragment/ContactAddFragment.java +++ b/xabber/src/main/java/com/xabber/android/ui/fragment/ContactAddFragment.java @@ -1,7 +1,6 @@ package com.xabber.android.ui.fragment; import android.app.Activity; -import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; @@ -14,12 +13,11 @@ import com.xabber.android.R; import com.xabber.android.data.Application; -import com.xabber.android.data.log.LogManager; import com.xabber.android.data.NetworkException; import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.entity.AccountJid; import com.xabber.android.data.entity.UserJid; -import com.xabber.android.data.message.MessageManager; +import com.xabber.android.data.log.LogManager; import com.xabber.android.data.roster.PresenceManager; import com.xabber.android.data.roster.RosterManager; import com.xabber.android.ui.adapter.AccountChooseAdapter; @@ -27,7 +25,11 @@ import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; +import java.util.ArrayList; import java.util.Collection; public class ContactAddFragment extends GroupEditorFragment @@ -172,6 +174,7 @@ public void addContact() { } String contactString = userView.getText().toString(); + contactString = contactString.replace(" ", ""); if (TextUtils.isEmpty(contactString)) { Toast.makeText(getActivity(), getString(R.string.EMPTY_USER_NAME), @@ -179,43 +182,69 @@ public void addContact() { return ; } - UserJid user; + final UserJid user; try { - user = UserJid.from(contactString); - } catch (UserJid.UserJidCreateException e) { - LogManager.exception(this, e); + EntityBareJid entityFullJid = JidCreate.entityBareFrom(contactString); + user = UserJid.from(entityFullJid); + } catch (XmppStringprepException | UserJid.UserJidCreateException e) { + e.printStackTrace(); + Toast.makeText(getActivity(), getString(R.string.INCORRECT_USER_NAME), Toast.LENGTH_LONG).show(); return; } LogManager.i(this, "user: " + user); - AccountJid account = (AccountJid) accountView.getSelectedItem(); + final AccountJid account = (AccountJid) accountView.getSelectedItem(); if (account == null) { Toast.makeText(getActivity(), getString(R.string.EMPTY_ACCOUNT), Toast.LENGTH_LONG).show(); return; } - try { - RosterManager.getInstance().createContact(account, user, - nameView.getText().toString(), getSelected()); - PresenceManager.getInstance().requestSubscription(account, user); - MessageManager.getInstance().openChat(account, user); - } catch (SmackException.NotLoggedInException | SmackException.NotConnectedException e) { - Application.getInstance().onError(R.string.NOT_CONNECTED); - } catch (XMPPException.XMPPErrorException e) { - Application.getInstance().onError(R.string.XMPP_EXCEPTION); - } catch (SmackException.NoResponseException e) { - Application.getInstance().onError(R.string.CONNECTION_FAILED); - } catch (NetworkException e) { - Application.getInstance().onError(e); - } catch (InterruptedException e) { - LogManager.exception(this, e); - } - getActivity().finish(); + listenerActivity.showProgress(true); + final String name = nameView.getText().toString(); + final ArrayList groups = getSelected(); + + Application.getInstance().runInBackgroundUserRequest(new Runnable() { + @Override + public void run() { + try { + RosterManager.getInstance().createContact(account, user, name, groups); + PresenceManager.getInstance().requestSubscription(account, user); + } catch (SmackException.NotLoggedInException | SmackException.NotConnectedException e) { + Application.getInstance().onError(R.string.NOT_CONNECTED); + stopAddContactProcess(false); + } catch (XMPPException.XMPPErrorException e) { + Application.getInstance().onError(R.string.XMPP_EXCEPTION); + stopAddContactProcess(false); + } catch (SmackException.NoResponseException e) { + Application.getInstance().onError(R.string.CONNECTION_FAILED); + stopAddContactProcess(false); + } catch (NetworkException e) { + Application.getInstance().onError(e); + stopAddContactProcess(false); + } catch (InterruptedException e) { + LogManager.exception(this, e); + stopAddContactProcess(false); + } + + stopAddContactProcess(true); + } + }); + } + + private void stopAddContactProcess(final boolean success) { + Application.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + listenerActivity.showProgress(false); + if (success) getActivity().finish(); + } + }); } public interface Listener { void onAccountSelected(AccountJid account); + void showProgress(boolean show); } } diff --git a/xabber/src/main/java/com/xabber/android/ui/helper/NewContactTitleInflater.java b/xabber/src/main/java/com/xabber/android/ui/helper/NewContactTitleInflater.java index 401556eaaa..1ad79c0f82 100644 --- a/xabber/src/main/java/com/xabber/android/ui/helper/NewContactTitleInflater.java +++ b/xabber/src/main/java/com/xabber/android/ui/helper/NewContactTitleInflater.java @@ -7,6 +7,7 @@ import android.widget.TextView; import com.xabber.android.R; +import com.xabber.android.data.account.StatusMode; import com.xabber.android.data.extension.avatar.AvatarManager; import com.xabber.android.data.extension.cs.ChatStateManager; import com.xabber.android.data.message.NotificationState; @@ -84,12 +85,12 @@ private static void setStatus(Context context, View titleView, AbstractContact a } else if (chatState == ChatState.paused) { statusText = context.getString(R.string.chat_state_paused); } else { - statusText = abstractContact.getStatusText().trim(); - if (statusText.toString().isEmpty()) { + if (StatusMode.unavailable == abstractContact.getStatusMode()) statusText = getLastActivity(abstractContact); - if (statusText.length() <= 0) - statusText = context.getString(abstractContact.getStatusMode().getStringID()); - } + else statusText = abstractContact.getStatusText().trim(); + + if (statusText.toString().isEmpty()) + statusText = context.getString(abstractContact.getStatusMode().getStringID()); } statusTextView.setText(statusText); } diff --git a/xabber/src/main/java/com/xabber/android/ui/preferences/ChannelSettingsFragment.java b/xabber/src/main/java/com/xabber/android/ui/preferences/ChannelSettingsFragment.java index 010acebf87..ee718c1a79 100644 --- a/xabber/src/main/java/com/xabber/android/ui/preferences/ChannelSettingsFragment.java +++ b/xabber/src/main/java/com/xabber/android/ui/preferences/ChannelSettingsFragment.java @@ -136,7 +136,8 @@ private String getSoundTitle(NotificationChannel channel) { if (channel == null) return null; Uri uri = channel.getSound(); Ringtone ringtone = RingtoneManager.getRingtone(getActivity(), uri); - return ringtone.getTitle(getActivity()); + if (ringtone != null) return ringtone.getTitle(getActivity()); + else return "Unknown ringtone"; } @Override @@ -144,7 +145,4 @@ protected void setNewRingtone(ChannelRingtoneHolder ringtoneHolder) { NotificationChannelUtils.updateMessageChannel(notificationManager, ringtoneHolder.type, Uri.parse(ringtoneHolder.uri), null, null); } -} - - - +} \ No newline at end of file diff --git a/xabber/src/main/java/com/xabber/android/ui/preferences/CustomNotifSettingsFragment.java b/xabber/src/main/java/com/xabber/android/ui/preferences/CustomNotifSettingsFragment.java index 9e4b0feb18..3837376423 100644 --- a/xabber/src/main/java/com/xabber/android/ui/preferences/CustomNotifSettingsFragment.java +++ b/xabber/src/main/java/com/xabber/android/ui/preferences/CustomNotifSettingsFragment.java @@ -123,13 +123,15 @@ private String getSoundTitle(NotificationChannel channel) { if (channel == null) return null; Uri uri = channel.getSound(); Ringtone ringtone = RingtoneManager.getRingtone(getActivity(), uri); - return ringtone.getTitle(getActivity()); + if (ringtone != null) return ringtone.getTitle(getActivity()); + else return "Unknown ringtone"; } private String getSoundTitle(NotifyPrefs notifyPrefs) { Uri uri = Uri.parse(notifyPrefs.getSound()); Ringtone ringtone = RingtoneManager.getRingtone(getActivity(), uri); - return ringtone.getTitle(getActivity()); + if (ringtone != null) return ringtone.getTitle(getActivity()); + else return "Unknown ringtone"; } private String getVibroSummary(Context context, NotifyPrefs notifyPrefs) { diff --git a/xabber/src/main/java/com/xabber/android/ui/preferences/NotificationsSettingsFragment.java b/xabber/src/main/java/com/xabber/android/ui/preferences/NotificationsSettingsFragment.java index fbc7f9e47d..e73f4c036d 100644 --- a/xabber/src/main/java/com/xabber/android/ui/preferences/NotificationsSettingsFragment.java +++ b/xabber/src/main/java/com/xabber/android/ui/preferences/NotificationsSettingsFragment.java @@ -5,29 +5,41 @@ import android.app.NotificationManager; import android.content.Context; import android.content.DialogInterface; +import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.widget.Toast; import com.xabber.android.R; import com.xabber.android.data.SettingsManager; +import com.xabber.android.data.notification.NotificationChannelUtils; import com.xabber.android.data.notification.custom_notification.CustomNotifyPrefsManager; import com.xabber.android.ui.activity.PreferenceSummaryHelperActivity; import static com.xabber.android.data.SettingsManager.NOTIFICATION_PREFERENCES; public class NotificationsSettingsFragment extends android.preference.PreferenceFragment { + + protected NotificationManager notificationManager; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getPreferenceManager().setSharedPreferencesName(NOTIFICATION_PREFERENCES); - addPreferencesFromResource(R.xml.preference_notifications); + notificationManager = (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE); PreferenceSummaryHelperActivity.updateSummary(getPreferenceScreen()); - Preference resetPreference = (Preference) getPreferenceScreen().findPreference(getString(R.string.events_reset_key)); - resetPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + Preference resetPreference = getPreferenceScreen().findPreference(getString(R.string.events_reset_key)); + if (resetPreference != null) setupResetPreferences(resetPreference, notificationManager); + + Preference removeCustomNotifPreference = getPreferenceScreen().findPreference(getString(R.string.events_remove_all_custom_key)); + if (removeCustomNotifPreference != null) setupRemoveCustomNotifPreferences(removeCustomNotifPreference, notificationManager); + } + + protected void setupResetPreferences(Preference preference, final NotificationManager notificationManager) { + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); @@ -37,6 +49,8 @@ public boolean onPreferenceClick(Preference preference) { public void onClick(DialogInterface dialog, int which) { Toast.makeText(getActivity(), R.string.events_reset_toast, Toast.LENGTH_SHORT).show(); SettingsManager.resetPreferences(getActivity(), NOTIFICATION_PREFERENCES); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + NotificationChannelUtils.resetMessageChannels(notificationManager); ((NotificationsSettings) getActivity()).restartFragment(); } }) @@ -50,35 +64,31 @@ public void onClick(DialogInterface dialog, int which) { return true; } }); + } - Preference removeCustomNotifPreference = getPreferenceScreen() - .findPreference(getString(R.string.events_remove_all_custom_key)); - if (removeCustomNotifPreference != null) { - removeCustomNotifPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(R.string.events_remove_all_custom_summary) - .setPositiveButton(R.string.remove, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - NotificationManager notificationManager = (NotificationManager) - getActivity().getSystemService(Context.NOTIFICATION_SERVICE); - Toast.makeText(getActivity(), R.string.events_reset_toast, Toast.LENGTH_SHORT).show(); - CustomNotifyPrefsManager.getInstance().deleteAllNotifyPrefs(notificationManager); - ((NotificationsSettings) getActivity()).restartFragment(); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - builder.create().show(); - return true; - } - }); - } + protected void setupRemoveCustomNotifPreferences(Preference preference, final NotificationManager notificationManager) { + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(R.string.events_remove_all_custom_summary) + .setPositiveButton(R.string.remove, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Toast.makeText(getActivity(), R.string.events_reset_toast, Toast.LENGTH_SHORT).show(); + CustomNotifyPrefsManager.getInstance().deleteAllNotifyPrefs(notificationManager); + ((NotificationsSettings) getActivity()).restartFragment(); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + builder.create().show(); + return true; + } + }); } } diff --git a/xabber/src/main/res/drawable/ic_message_acknowledged_14dp.xml b/xabber/src/main/res/drawable/ic_message_acknowledged_14dp.xml index edffcca84d..65fe1fba3e 100644 --- a/xabber/src/main/res/drawable/ic_message_acknowledged_14dp.xml +++ b/xabber/src/main/res/drawable/ic_message_acknowledged_14dp.xml @@ -1,4 +1,4 @@ - + diff --git a/xabber/src/main/res/drawable/ic_message_delivered_14dp.xml b/xabber/src/main/res/drawable/ic_message_delivered_14dp.xml index 020ff956be..edffcca84d 100644 --- a/xabber/src/main/res/drawable/ic_message_delivered_14dp.xml +++ b/xabber/src/main/res/drawable/ic_message_delivered_14dp.xml @@ -1,4 +1,4 @@ - + diff --git a/xabber/src/main/res/drawable/ic_message_displayed.xml b/xabber/src/main/res/drawable/ic_message_displayed.xml new file mode 100644 index 0000000000..020ff956be --- /dev/null +++ b/xabber/src/main/res/drawable/ic_message_displayed.xml @@ -0,0 +1,4 @@ + + + diff --git a/xabber/src/main/res/drawable/ic_message_forwarded_14dp.xml b/xabber/src/main/res/drawable/ic_message_forwarded_14dp.xml index 97a5c0b1a9..5dfd1664e1 100644 --- a/xabber/src/main/res/drawable/ic_message_forwarded_14dp.xml +++ b/xabber/src/main/res/drawable/ic_message_forwarded_14dp.xml @@ -1,8 +1,7 @@ - - + \ No newline at end of file diff --git a/xabber/src/main/res/layout/activity_with_toolbar_progress_and_container.xml b/xabber/src/main/res/layout/activity_with_toolbar_progress_and_container.xml new file mode 100644 index 0000000000..07f2f6b4a0 --- /dev/null +++ b/xabber/src/main/res/layout/activity_with_toolbar_progress_and_container.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/xabber/src/main/res/layout/item_message.xml b/xabber/src/main/res/layout/item_message.xml index 2a87311b40..ef28129cce 100644 --- a/xabber/src/main/res/layout/item_message.xml +++ b/xabber/src/main/res/layout/item_message.xml @@ -131,8 +131,7 @@ android:id="@+id/message_status_icon" android:layout_width="14dp" android:layout_height="14dp" - android:src="@drawable/ic_message_synced_14dp" - android:alpha="0.5"/> + android:src="@drawable/ic_message_synced_14dp" /> + android:src="@drawable/ic_message_synced_14dp" /> + + + + + + diff --git a/xabber/src/main/res/values-ru-rRU/preference_editor.xml b/xabber/src/main/res/values-ru-rRU/preference_editor.xml index f29daa8094..e69a605d81 100644 --- a/xabber/src/main/res/values-ru-rRU/preference_editor.xml +++ b/xabber/src/main/res/values-ru-rRU/preference_editor.xml @@ -46,6 +46,7 @@ Отключить графические смайлы Настройки подключения\nНастройки вариантов подключения Список контактов\nНастройки отображения списка контактов + Настройки отображения чата Настройки отладки\nВыберите этот пункт, если хотите помочь разработчикам сделать Xabber ещё лучше Настройки Оповещения\nНастройки вариантов оповещения @@ -142,7 +143,7 @@ Все настройки уведомлений будут установлены в значения по умолчанию Сбросить настройки уведомлений? Настройки были сброшены - Удалить все спецтальные настройки уведомлений + Удалить все специальные настройки уведомлений Все специальные настройки уведомлений (для контактов, групп и аккаунтов) будут удалены Мелодия не доступна diff --git a/xabber/src/main/res/values/chat_viewer.xml b/xabber/src/main/res/values/chat_viewer.xml index a0a4167d3b..dc6a529d46 100644 --- a/xabber/src/main/res/values/chat_viewer.xml +++ b/xabber/src/main/res/values/chat_viewer.xml @@ -100,6 +100,7 @@ Error description: Sent Delivered + Displayed Received from history Error Sent from another device diff --git a/xabber/src/main/res/values/notification_bar.xml b/xabber/src/main/res/values/notification_bar.xml index eb1c686452..5e33f5b0ed 100644 --- a/xabber/src/main/res/values/notification_bar.xml +++ b/xabber/src/main/res/values/notification_bar.xml @@ -59,5 +59,7 @@ Persistent connection Persistent notification for establish connection to receive messages Events + Silent message notifications Notifications for events like: group chat invites, subscription requests, OTR requests + Silent notifications about messages \ No newline at end of file diff --git a/xabber/src/main/res/values/preference_editor.xml b/xabber/src/main/res/values/preference_editor.xml index 285311e8f3..61c4304065 100644 --- a/xabber/src/main/res/values/preference_editor.xml +++ b/xabber/src/main/res/values/preference_editor.xml @@ -54,6 +54,7 @@ Connection settings\nConnection settings Contact list\nCustomize contact list appearance + Customize chat appearance Debug settings\nSettings to help developers improve Xabber Settings Notifications\nNotification settings diff --git a/xabber/src/main/res/xml/preference_editor.xml b/xabber/src/main/res/xml/preference_editor.xml index c8e5c61282..3efcbf78ca 100644 --- a/xabber/src/main/res/xml/preference_editor.xml +++ b/xabber/src/main/res/xml/preference_editor.xml @@ -25,6 +25,7 @@ />