/** * Copyright (c) 2013, Redsolution LTD. All rights reserved. * * This file is part of Xabber project; you can redistribute it and/or * modify it under the terms of the GNU General Public License, Version 3. * * Xabber is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License, * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.xabber.android.data.extension.cs; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Handler; 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.connection.ConnectionItem; import com.xabber.android.data.connection.ConnectionManager; import com.xabber.android.data.connection.StanzaSender; import com.xabber.android.data.connection.listeners.OnDisconnectListener; import com.xabber.android.data.connection.listeners.OnPacketListener; import com.xabber.android.data.entity.AccountJid; import com.xabber.android.data.entity.NestedMap; import com.xabber.android.data.entity.NestedNestedMaps; import com.xabber.android.data.entity.UserJid; import com.xabber.android.data.extension.muc.RoomChat; import com.xabber.android.data.message.AbstractChat; import com.xabber.android.data.message.MessageManager; import com.xabber.android.data.roster.RosterManager; import com.xabber.android.receiver.ComposingPausedReceiver; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence.Type; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.parts.Resourcepart; import java.util.Calendar; import java.util.Map; /** * Provide information about chat state. * * @author alexander.ivanov */ public class ChatStateManager implements OnDisconnectListener, OnPacketListener, OnCloseListener { private static ChatStateManager instance; private static final int PAUSE_TIMEOUT = 30 * 1000; private static final long REMOVE_STATE_DELAY = 10 * 1000; static { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { @Override public void connectionCreated(final XMPPConnection connection) { ServiceDiscoveryManager.getInstanceFor(connection) .addFeature("http://jabber.org/protocol/chatstates"); } }); } public static ChatStateManager getInstance() { if (instance == null) { instance = new ChatStateManager(); } return instance; } /** * Chat states for lower cased resource for bareAddress in account. */ private final NestedNestedMaps<Resourcepart, ChatState> chatStates; /** * Cleaners for chat states for lower cased resource for bareAddress in * account. */ private final NestedNestedMaps<Resourcepart, Runnable> stateCleaners; /** * Information about chat state notification support for lower cased * resource for bareAddress in account. */ private final NestedNestedMaps<Resourcepart, Boolean> supports; /** * Sent chat state notifications for bareAddress in account. */ private final NestedMap<ChatState> sent; /** * Scheduled pause intents for bareAddress in account. */ private final NestedMap<PendingIntent> pauseIntents; /** * Alarm manager. */ private final AlarmManager alarmManager; /** * Handler for clear states on timeout. */ private final Handler handler; private ChatStateManager() { chatStates = new NestedNestedMaps<>(); stateCleaners = new NestedNestedMaps<>(); supports = new NestedNestedMaps<>(); sent = new NestedMap<>(); pauseIntents = new NestedMap<>(); alarmManager = (AlarmManager) Application.getInstance() .getSystemService(Context.ALARM_SERVICE); handler = new Handler(); } /** * Returns best information chat state for specified bare address. * * @return <code>null</code> if there is no available information. */ public ChatState getChatState(AccountJid account, UserJid bareAddress) { Map<Resourcepart, ChatState> map = chatStates.get(account.toString(), bareAddress.toString()); if (map == null) { return null; } ChatState chatState = null; for (ChatState check : map.values()) { if (chatState == null || check.compareTo(chatState) < 0) { chatState = check; } } return chatState; } /** * Whether sending chat notification for specified chat is supported. */ private boolean isSupported(AbstractChat chat, boolean outgoingMessage) { if (chat instanceof RoomChat) return false; Jid to = chat.getTo(); BareJid bareAddress = to.asBareJid(); Resourcepart resource = to.getResourceOrNull(); Map<Resourcepart, Boolean> map = supports.get(chat.getAccount().toString(), bareAddress.toString()); if (map != null) { if (resource!= null && !resource.equals(Resourcepart.EMPTY)) { Boolean value = map.get(resource); if (value != null) return value; } else { if (outgoingMessage) return true; for (Boolean value : map.values()) if (value != null && value) return true; } } return outgoingMessage; } /** * Update outgoing message before sending. */ public void updateOutgoingMessage(AbstractChat chat, Message message) { if (!isSupported(chat, true)) { return; } message.addExtension(new ChatStateExtension(ChatState.active)); sent.put(chat.getAccount().toString(), chat.getUser().toString(), ChatState.active); cancelPauseIntent(chat.getAccount(), chat.getUser()); } /** * Update chat state information and send message if necessary. */ private void updateChatState(AccountJid account, UserJid user, ChatState chatState) { if (!SettingsManager.chatsStateNotification() || sent.get(account.toString(), user.toString()) == chatState) { return; } AbstractChat chat = MessageManager.getInstance().getChat(account, user); if (chat == null || !isSupported(chat, false)) { return; } sent.put(chat.getAccount().toString(), chat.getUser().toString(), chatState); Message message = new Message(); message.setType(chat.getType()); message.setTo(chat.getTo()); message.addExtension(new ChatStateExtension(chatState)); try { StanzaSender.sendStanza(account, message); } catch (NetworkException e) { // Just ignore it. } } /** * Cancel pause intent from the schedule. */ private void cancelPauseIntent(AccountJid account, UserJid user) { PendingIntent pendingIntent = pauseIntents.remove(account.toString(), user.toString()); if (pendingIntent != null) alarmManager.cancel(pendingIntent); } /** * Must be call each time user change text message. */ public void onComposing(AccountJid account, UserJid user, CharSequence text) { cancelPauseIntent(account, user); if (text.length() == 0) { updateChatState(account, user, ChatState.active); return; } else { updateChatState(account, user, ChatState.composing); } Intent intent = ComposingPausedReceiver.createIntent( Application.getInstance(), account, user); PendingIntent pendingIntent = PendingIntent.getBroadcast( Application.getInstance(), 0, intent, 0); Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); calendar.add(Calendar.MILLISECOND, PAUSE_TIMEOUT); alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent); pauseIntents.put(account.toString(), user.toString(), pendingIntent); } public void onPaused(AccountJid account, UserJid user) { if (account == null || user == null) return; if (sent.get(account.toString(), user.toString()) != ChatState.composing) { return; } updateChatState(account, user, ChatState.paused); pauseIntents.remove(account.toString(), user.toString()); } @Override public void onDisconnect(ConnectionItem connection) { if (!(connection instanceof AccountItem)) return; AccountJid account = ((AccountItem) connection).getAccount(); chatStates.clear(account.toString()); for (Map<Resourcepart, Runnable> map : stateCleaners.getNested(account.toString()).values()) { for (Runnable runnable : map.values()) { handler.removeCallbacks(runnable); } } stateCleaners.clear(account.toString()); supports.clear(account.toString()); sent.clear(account.toString()); for (PendingIntent pendingIntent : pauseIntents.getNested(account.toString()).values()) { alarmManager.cancel(pendingIntent); } pauseIntents.clear(account.toString()); } private void removeCallback(AccountJid account, BareJid bareAddress, Resourcepart resource) { Runnable runnable = stateCleaners.remove(account.toString(), bareAddress.toString(), resource); if (runnable != null) { handler.removeCallbacks(runnable); } } @Override public void onStanza(ConnectionItem connection, Stanza stanza) { if (stanza.getFrom() == null) { return; } final Resourcepart resource = stanza.getFrom().getResourceOrNull(); if (resource == null) { return; } final AccountJid account = ((AccountItem) connection).getAccount(); final UserJid bareUserJid; try { bareUserJid = UserJid.from(stanza.getFrom()).getBareUserJid(); } catch (UserJid.UserJidCreateException e) { return; } if (stanza instanceof Presence) { Presence presence = (Presence) stanza; if (presence.getType() != Type.unavailable) { return; } chatStates.remove(account.toString(), bareUserJid.toString(), resource); removeCallback(account, bareUserJid.getBareJid(), resource); supports.remove(account.toString(), bareUserJid.toString(), resource); } else if (stanza instanceof Message) { boolean support = false; for (ExtensionElement extension : stanza.getExtensions()) if (extension instanceof ChatStateExtension) { removeCallback(account, bareUserJid.getBareJid(), resource); ChatState chatState = ((ChatStateExtension) extension).getChatState(); chatStates.put(account.toString(), bareUserJid.toString(), resource, chatState); if (chatState != ChatState.active) { Runnable runnable = new Runnable() { @Override public void run() { if (this != stateCleaners.get(account.toString(), bareUserJid.toString(), resource)) { return; } chatStates.remove(account.toString(), bareUserJid.toString(), resource); removeCallback(account, bareUserJid.getBareJid(), resource); RosterManager.onContactChanged(account, bareUserJid); } }; handler.postDelayed(runnable, REMOVE_STATE_DELAY); stateCleaners.put(account.toString(), bareUserJid.toString(), resource, runnable); } RosterManager.onContactChanged(account, bareUserJid); support = true; break; } Message message = (Message) stanza; if (message.getType() != Message.Type.chat && message.getType() != Message.Type.groupchat) { return; } if (support) { supports.put(account.toString(), bareUserJid.toString(), resource, true); } else if (supports.get(account.toString(), bareUserJid.toString(), resource) == null) { // Disable only if there no information about support. supports.put(account.toString(), bareUserJid.toString(), resource, false); } } } @Override public void onClose() { for (PendingIntent pendingIntent : pauseIntents.values()) { alarmManager.cancel(pendingIntent); } pauseIntents.clear(); } }