/**
* 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.vcard;
import android.database.Cursor;
import com.xabber.android.data.Application;
import com.xabber.android.data.log.LogManager;
import com.xabber.android.data.NetworkException;
import com.xabber.android.data.OnLoadListener;
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.account.listeners.OnAccountRemovedListener;
import com.xabber.android.data.connection.ConnectionItem;
import com.xabber.android.data.connection.ConnectionManager;
import com.xabber.android.data.connection.listeners.OnPacketListener;
import com.xabber.android.data.database.sqlite.VCardTable;
import com.xabber.android.data.entity.AccountJid;
import com.xabber.android.data.entity.UserJid;
import com.xabber.android.data.extension.avatar.AvatarManager;
import com.xabber.android.data.extension.blocking.BlockingManager;
import com.xabber.android.data.extension.muc.MUCManager;
import com.xabber.android.data.roster.OnRosterChangedListener;
import com.xabber.android.data.roster.OnRosterReceivedListener;
import com.xabber.android.data.roster.PresenceManager;
import com.xabber.android.data.roster.RosterContact;
import com.xabber.android.data.roster.RosterManager;
import com.xabber.android.data.roster.StructuredName;
import com.xabber.xmpp.vcard.VCardProperty;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.vcardtemp.packet.VCard;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
/**
* Manage vCards and there requests.
*
* @author alexander.ivanov
*/
public class VCardManager implements OnLoadListener, OnPacketListener,
OnRosterReceivedListener, OnAccountRemovedListener {
private static final StructuredName EMPTY_STRUCTURED_NAME = new StructuredName(
null, null, null, null, null);
/**
* Nick and formatted names for the users.
*/
private final Map<Jid, StructuredName> names;
/**
* List of accounts which requests its avatar in order to avoid subsequence
* requests.
*/
private final ArrayList<AccountJid> accountRequested;
private static VCardManager instance;
@SuppressWarnings("WeakerAccess")
Set<Jid> vCardRequests = new ConcurrentSkipListSet<>();
@SuppressWarnings("WeakerAccess")
Set<AccountJid> vCardSaveRequests = new ConcurrentSkipListSet<>();
public static VCardManager getInstance() {
if (instance == null) {
instance = new VCardManager();
}
return instance;
}
private VCardManager() {
names = new HashMap<>();
accountRequested = new ArrayList<>();
}
@Override
public void onLoad() {
final Map<Jid, StructuredName> names = new HashMap<>();
Cursor cursor = VCardTable.getInstance().list();
try {
if (cursor.moveToFirst()) {
do {
try {
names.put(
JidCreate.from(VCardTable.getUser(cursor)),
new StructuredName(VCardTable.getNickName(cursor),
VCardTable.getFormattedName(cursor),
VCardTable.getFirstName(cursor), VCardTable
.getMiddleName(cursor), VCardTable
.getLastName(cursor)));
} catch (XmppStringprepException e) {
LogManager.exception(this, e);
}
} while (cursor.moveToNext());
}
} finally {
cursor.close();
}
Application.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
onLoaded(names);
}
});
}
@SuppressWarnings("WeakerAccess")
void onLoaded(Map<Jid, StructuredName> names) {
this.names.putAll(names);
}
@Override
public void onRosterReceived(AccountItem accountItem) {
AccountJid account = accountItem.getAccount();
if (!accountRequested.contains(account) && SettingsManager.connectionLoadVCard()) {
BareJid bareAddress = accountItem.getRealJid().asBareJid();
if (bareAddress != null && !names.containsKey(bareAddress)) {
request(account, bareAddress);
accountRequested.add(account);
}
}
Collection<UserJid> blockedContacts = BlockingManager.getInstance().getBlockedContacts(account);
Collection<RosterContact> accountRosterContacts = RosterManager.getInstance().getAccountRosterContacts(account);
// Request vCards for new contacts.
for (RosterContact contact : accountRosterContacts) {
if (!names.containsKey(contact.getUser().getJid())) {
if (!blockedContacts.contains(contact.getUser())) {
request(account, contact.getUser().getJid());
}
}
}
}
@Override
public void onAccountRemoved(AccountItem accountItem) {
accountRequested.remove(accountItem.getAccount());
}
public void requestByUser(final AccountJid account, final Jid jid) {
Application.getInstance().runInBackgroundUserRequest(new Runnable() {
@Override
public void run() {
getVCard(account, jid);
}
});
}
/**
* Requests vCard.
*/
public void request(final AccountJid account, final Jid jid) {
Application.getInstance().runInBackground(new Runnable() {
@Override
public void run() {
getVCard(account, jid);
}
});
}
/**
* Get uses's nick name.
*
* @return first specified value:
* <ul>
* <li>nick name</li>
* <li>formatted name</li>
* <li>empty string</li>
* </ul>
*/
public String getName(Jid jid) {
StructuredName name = names.get(jid);
if (name == null)
return "";
return name.getBestName();
}
/**
* Get uses's name information.
*
* @return <code>null</code> if there is no info.
*/
public StructuredName getStructuredName(Jid jid) {
return names.get(jid);
}
@SuppressWarnings("WeakerAccess")
void onVCardReceived(final AccountJid account, final Jid bareAddress, final VCard vCard) {
final StructuredName name;
if (vCard.getType() == Type.error) {
onVCardFailed(account, bareAddress);
if (names.containsKey(bareAddress)) {
return;
}
name = EMPTY_STRUCTURED_NAME;
} else {
try {
String hash = vCard.getAvatarHash();
byte[] avatar = vCard.getAvatar();
AvatarManager.getInstance().onAvatarReceived(bareAddress, hash, avatar);
// "bad base-64" error happen sometimes
} catch (IllegalArgumentException e) {
LogManager.exception(this, e);
}
name = new StructuredName(vCard.getNickName(), vCard.getField(VCardProperty.FN.name()),
vCard.getFirstName(), vCard.getMiddleName(), vCard.getLastName());
try {
if (account.getFullJid().asBareJid().equals(bareAddress.asBareJid())) {
PresenceManager.getInstance().resendPresence(account);
}
} catch (NetworkException e) {
LogManager.exception(this, e);
}
}
names.put(bareAddress, name);
RosterContact rosterContact = RosterManager.getInstance()
.getRosterContact(account, bareAddress.asBareJid());
for (OnRosterChangedListener listener : Application.getInstance()
.getManagers(OnRosterChangedListener.class)) {
listener.onContactStructuredInfoChanged(rosterContact, name);
}
Application.getInstance().runInBackground(new Runnable() {
@Override
public void run() {
VCardTable.getInstance().write(bareAddress.toString(), name);
}
});
if (vCard.getFrom() == null) { // account it self
AccountManager.getInstance().onAccountChanged(account);
} else {
try {
RosterManager.onContactChanged(account, UserJid.from(bareAddress));
} catch (UserJid.UserJidCreateException e) {
LogManager.exception(this, e);
}
}
for (OnVCardListener listener : Application.getInstance().getUIListeners(OnVCardListener.class)) {
listener.onVCardReceived(account, bareAddress, vCard);
}
}
@SuppressWarnings("WeakerAccess")
void onVCardFailed(final AccountJid account, final Jid bareAddress) {
for (OnVCardListener listener : Application.getInstance().getUIListeners(OnVCardListener.class)) {
listener.onVCardFailed(account, bareAddress);
}
}
@SuppressWarnings("WeakerAccess")
void onVCardSaveSuccess(AccountJid account) {
for (OnVCardSaveListener listener : Application.getInstance().getUIListeners(OnVCardSaveListener.class)) {
listener.onVCardSaveSuccess(account);
}
}
@SuppressWarnings("WeakerAccess")
void onVCardSaveFailed(AccountJid account) {
for (OnVCardSaveListener listener : Application.getInstance().getUIListeners(OnVCardSaveListener.class)) {
listener.onVCardSaveFailed(account);
}
}
@Override
public void onStanza(ConnectionItem connection, Stanza stanza) {
if (!(connection instanceof AccountItem)) {
return;
}
AccountJid account = connection.getAccount();
if (stanza instanceof Presence && ((Presence) stanza).getType() != Presence.Type.error) {
Jid from = stanza.getFrom();
if (from == null) {
return;
}
Jid addressForVcard = from;
if (MUCManager.getInstance().hasRoom(account, from.asEntityBareJidIfPossible())) {
addressForVcard = from;
}
// Request vCard for new users
if (!names.containsKey(addressForVcard)) {
if (SettingsManager.connectionLoadVCard()) {
request(account, addressForVcard);
}
}
}
}
@SuppressWarnings("WeakerAccess")
void getVCard(final AccountJid account, final Jid srcUser) {
final AccountItem accountItem = AccountManager.getInstance().getAccount(account);
if (accountItem == null) {
onVCardFailed(account, srcUser);
return;
}
final org.jivesoftware.smackx.vcardtemp.VCardManager vCardManager
= org.jivesoftware.smackx.vcardtemp.VCardManager.getInstanceFor(accountItem.getConnection());
if (!accountItem.getConnection().isAuthenticated()) {
onVCardFailed(account, srcUser);
return;
}
VCard vCard = null;
Collection<UserJid> blockedContacts = BlockingManager.getInstance().getBlockedContacts(account);
for (UserJid blockedContact : blockedContacts) {
if (blockedContact.getBareJid().equals(srcUser.asBareJid())) {
return;
}
}
final EntityBareJid entityBareJid = srcUser.asEntityBareJidIfPossible();
if (entityBareJid != null) {
vCardRequests.add(srcUser);
try {
vCard = vCardManager.loadVCard(entityBareJid);
} catch (SmackException.NoResponseException | SmackException.NotConnectedException e) {
LogManager.exception(this, e);
LogManager.w(this, "Error getting vCard: " + e.getMessage());
} catch (XMPPException.XMPPErrorException e ) {
LogManager.exception(this, e);
LogManager.w(this, "XMPP error getting vCard: " + e.getMessage() + e.getXMPPError());
if (e.getXMPPError().getCondition() == XMPPError.Condition.item_not_found) {
vCard = new VCard();
}
} catch (ClassCastException e) {
LogManager.exception(this, e);
// http://stackoverflow.com/questions/31498721/error-loading-vcard-information-using-smack-emptyresultiq-cannot-be-cast-to-or
LogManager.w(this, "ClassCastException: " + e.getMessage());
vCard = new VCard();
} catch (InterruptedException e) {
LogManager.exception(this, e);
}
vCardRequests.remove(srcUser);
}
final VCard finalVCard = vCard;
Application.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
if (finalVCard == null) {
onVCardFailed(account, srcUser);
} else {
onVCardReceived(account, srcUser, finalVCard);
}
}
});
}
public void saveVCard(final AccountJid account, final VCard vCard) {
AccountItem accountItem = AccountManager.getInstance().getAccount(account);
if (accountItem == null) {
onVCardSaveFailed(account);
return;
}
final AbstractXMPPConnection xmppConnection = accountItem.getConnection();
final org.jivesoftware.smackx.vcardtemp.VCardManager vCardManager = org.jivesoftware.smackx.vcardtemp.VCardManager.getInstanceFor(xmppConnection);
Application.getInstance().runInBackgroundUserRequest(new Runnable() {
@Override
public void run() {
boolean isSuccess = true;
xmppConnection.setPacketReplyTimeout(120000);
vCardSaveRequests.add(account);
try {
vCardManager.saveVCard(vCard);
String avatarHash = null;
try {
avatarHash = vCard.getAvatarHash();
// "bad base-64" error happen sometimes
} catch (IllegalArgumentException e) {
LogManager.exception(this, e);
}
if (avatarHash == null) {
avatarHash = AvatarManager.EMPTY_HASH;
}
PresenceManager.getInstance().sendVCardUpdatePresence(account, avatarHash);
} catch (SmackException.NoResponseException | XMPPException.XMPPErrorException
| SmackException.NotConnectedException | NetworkException | InterruptedException e) {
LogManager.w(this, "Error saving vCard: " + e.getMessage());
isSuccess = false;
}
vCardSaveRequests.remove(account);
xmppConnection.setPacketReplyTimeout(ConnectionManager.PACKET_REPLY_TIMEOUT);
final boolean finalIsSuccess = isSuccess;
Application.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
if (finalIsSuccess) {
onVCardSaveSuccess(account);
} else {
onVCardSaveFailed(account);
}
}
});
}
});
}
public boolean isVCardRequested(Jid user) {
return vCardRequests.contains(user.asBareJid());
}
public boolean isVCardSaveRequested(AccountJid account) {
return vCardSaveRequests.contains(account);
}
}