/**
* 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.avatar;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.xabber.android.R;
import com.xabber.android.data.Application;
import com.xabber.android.data.log.LogManager;
import com.xabber.android.data.OnLoadListener;
import com.xabber.android.data.OnLowMemoryListener;
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.listeners.OnPacketListener;
import com.xabber.android.data.database.sqlite.AvatarTable;
import com.xabber.android.data.entity.AccountJid;
import com.xabber.android.data.entity.UserJid;
import com.xabber.android.data.extension.vcard.VCardManager;
import com.xabber.android.ui.color.ColorManager;
import com.xabber.xmpp.vcardupdate.VCardUpdate;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/**
* Provides information about avatars (hashes and values). Store and retrieve
* hashes from database and binary values from file system. Caches user's hashes
* and avatar's values in memory. Handles changes in user's hashes. Requests
* information from server when avatar for given hash don't exists locally.
* <p/>
* <p/>
* This class is thread safe. All operation modification made from synchronized
* blocks.
* <p/>
* <p/>
* All requests to database / file system made in background thread or on
* application load.
*
* @author alexander.ivanov
*/
public class AvatarManager implements OnLoadListener, OnLowMemoryListener, OnPacketListener {
/**
* Maximum image width / height to be loaded.
*/
private static final int MAX_SIZE = 256;
public static final String EMPTY_HASH = "";
private static final Bitmap EMPTY_BITMAP = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
private static AvatarManager instance;
private final Application application;
/**
* Map with hashes for specified users.
* <p/>
* {@link #EMPTY_HASH} is used to store <code>null</code> values.
*/
private final Map<Jid, String> hashes;
/**
* Map with bitmaps for specified hashes.
* <p/>
* {@link #EMPTY_BITMAP} is used to store <code>null</code> values.
*/
private final Map<String, Bitmap> bitmaps;
/**
* Map with drawable used in contact list only for specified uses.
*/
private final Map<Jid, Drawable> contactListDrawables;
/**
* Users' default avatar set.
*/
private final BaseAvatarSet userAvatarSet;
/**
* Rooms' default avatar set.
*/
private final BaseAvatarSet roomAvatarSet;
public static AvatarManager getInstance() {
if (instance == null) {
instance = new AvatarManager();
}
return instance;
}
private AvatarManager() {
this.application = Application.getInstance();
userAvatarSet = new BaseAvatarSet(application, R.array.default_avatars_icons, R.array.default_avatars_colors);
roomAvatarSet = new BaseAvatarSet(application, R.array.muc_avatars, R.array.default_avatars_colors);
hashes = new HashMap<>();
bitmaps = new HashMap<>();
contactListDrawables = new HashMap<>();
}
/**
* Make {@link Bitmap} from array of bytes.
*
* @param value
* @return Bitmap. <code>null</code> can be returned if value is invalid or
* is <code>null</code>.
*/
private static Bitmap makeBitmap(byte[] value) {
if (value == null) {
return null;
}
// Load only size values
BitmapFactory.Options sizeOptions = new BitmapFactory.Options();
sizeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(value, 0, value.length, sizeOptions);
// Calculate factor to down scale image
int scale = 1;
int width_tmp = sizeOptions.outWidth;
int height_tmp = sizeOptions.outHeight;
while (width_tmp / 2 >= MAX_SIZE && height_tmp / 2 >= MAX_SIZE) {
scale *= 2;
width_tmp /= 2;
height_tmp /= 2;
}
// Load image
BitmapFactory.Options resultOptions = new BitmapFactory.Options();
resultOptions.inSampleSize = scale;
return BitmapFactory.decodeByteArray(value, 0, value.length, resultOptions);
}
public static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
int width = drawable.getIntrinsicWidth();
width = width > 0 ? width : 1;
int height = drawable.getIntrinsicHeight();
height = height > 0 ? height : 1;
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
@Override
public void onLoad() {
final Map<Jid, String> hashes = new HashMap<>();
final Map<String, Bitmap> bitmaps = new HashMap<>();
Cursor cursor = AvatarTable.getInstance().list();
try {
if (cursor.moveToFirst()) {
do {
String hash = AvatarTable.getHash(cursor);
try {
Jid jid = JidCreate.from(AvatarTable.getUser(cursor));
hashes.put(jid, hash == null ? EMPTY_HASH : hash);
} catch (XmppStringprepException e) {
LogManager.exception(this, e);
}
} while (cursor.moveToNext());
}
} finally {
cursor.close();
}
for (String hash : new HashSet<>(hashes.values()))
if (!hash.equals(EMPTY_HASH)) {
Bitmap bitmap = makeBitmap(AvatarStorage.getInstance().read(hash));
bitmaps.put(hash, bitmap == null ? EMPTY_BITMAP : bitmap);
}
Application.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
onLoaded(hashes, bitmaps);
}
});
}
private void onLoaded(Map<Jid, String> hashes, Map<String, Bitmap> bitmaps) {
this.hashes.putAll(hashes);
this.bitmaps.putAll(bitmaps);
}
/**
* Sets avatar's hash for user.
*
* @param jid
* @param hash can be <code>null</code>.
*/
private void setHash(final Jid jid, final String hash) {
hashes.put(jid, hash == null ? EMPTY_HASH : hash);
contactListDrawables.remove(jid);
application.runInBackground(new Runnable() {
@Override
public void run() {
AvatarTable.getInstance().write(jid.toString(), hash);
}
});
}
/**
* Get avatar's value for user.
*
* @param jid
* @return avatar's value. <code>null</code> can be returned if user has no
* avatar or avatar doesn't exists.
*/
private Bitmap getBitmap(Jid jid) {
String hash = getHash(jid);
if (hash == null || hash.equals(EMPTY_HASH)) {
return null;
}
Bitmap bitmap = bitmaps.get(hash);
if (bitmap == EMPTY_BITMAP) {
return null;
} else {
return bitmap;
}
}
@Nullable
public String getHash(Jid bareAddress) {
return hashes.get(bareAddress);
}
/**
* Sets avatar's value.
*
* @param hash
* @param value
*/
private void setValue(final String hash, final byte[] value) {
if (hash == null) {
return;
}
Bitmap bitmap = makeBitmap(value);
bitmaps.put(hash, bitmap == null ? EMPTY_BITMAP : bitmap);
application.runInBackground(new Runnable() {
@Override
public void run() {
AvatarStorage.getInstance().write(hash, value);
}
});
}
@Override
public void onLowMemory() {
contactListDrawables.clear();
userAvatarSet.onLowMemory();
roomAvatarSet.onLowMemory();
}
/**
* Gets account's avatar.
*
* @param account
* @return Avatar or default avatar if:
* <ul>
* <li>account has no avatar.</li>
* </ul>
*/
public Drawable getAccountAvatar(AccountJid account) {
Bitmap value = getBitmap(account.getFullJid().asBareJid());
if (value != null) {
return new BitmapDrawable(application.getResources(), value);
} else {
return getDefaultAccountAvatar(account);
}
}
@NonNull
public Drawable getDefaultAccountAvatar(AccountJid account) {
Drawable[] layers = new Drawable[2];
layers[0] = new ColorDrawable(ColorManager.getInstance().getAccountPainter().getAccountMainColor(account));
layers[1] = application.getResources().getDrawable(R.drawable.ic_avatar_1);
return new LayerDrawable(layers);
}
/**
* Gets avatar for regular user.
*
* @param user
* @return
*/
public Drawable getUserAvatar(UserJid user) {
Bitmap value = getBitmap(user.getJid());
if (value != null) {
return new BitmapDrawable(application.getResources(), value);
} else {
return getDefaultAvatarDrawable(userAvatarSet.getResourceId(user));
}
}
private Drawable getDefaultAvatarDrawable(BaseAvatarSet.DefaultAvatar defaultAvatar) {
Drawable[] layers = new Drawable[2];
layers[0] = new ColorDrawable(defaultAvatar.getBackgroundColor());
layers[1] = application.getResources().getDrawable(defaultAvatar.getIconResource());
return new LayerDrawable(layers);
}
/**
* Gets bitmap with avatar for regular user.
*
* @param user
* @return
*/
public Bitmap getUserBitmap(UserJid user) {
Bitmap value = getBitmap(user.getJid());
if (value != null) {
return value;
} else {
return drawableToBitmap(getDefaultAvatarDrawable(userAvatarSet.getResourceId(user)));
}
}
/**
* Gets and caches drawable with avatar for regular user.
*
* @param user
* @return
*/
public Drawable getUserAvatarForContactList(UserJid user) {
Drawable drawable = contactListDrawables.get(user.getJid());
if (drawable == null) {
drawable = getUserAvatar(user);
contactListDrawables.put(user.getJid(), drawable);
}
return drawable;
}
/**
* Gets avatar for the room.
*
* @param user
* @return
*/
public Drawable getRoomAvatar(UserJid user) {
return getDefaultAvatarDrawable(roomAvatarSet.getResourceId(user));
}
/**
* Gets bitmap for the room.
*
* @param user
* @return
*/
public Bitmap getRoomBitmap(UserJid user) {
return drawableToBitmap(getRoomAvatar(user));
}
/**
* Gets and caches drawable with room's avatar.
*
* @param user
* @return
*/
public Drawable getRoomAvatarForContactList(UserJid user) {
Drawable drawable = contactListDrawables.get(user.getJid());
if (drawable == null) {
drawable = getRoomAvatar(user);
contactListDrawables.put(user.getJid(), drawable);
}
return drawable;
}
/**
* Gets avatar for occupant in the room.
*
* @param user
* @return
*/
public Drawable getOccupantAvatar(UserJid user) {
return getDefaultAvatarDrawable(userAvatarSet.getResourceId(user));
}
/**
* Avatar was received.
*
* @param jid
* @param hash
* @param value
*/
public void onAvatarReceived(Jid jid, String hash, byte[] value) {
setValue(hash, value);
setHash(jid, hash);
}
@Override
public void onStanza(ConnectionItem connection, Stanza stanza) {
if (!(stanza instanceof Presence)) {
return;
}
AccountJid account = ((AccountItem) connection).getAccount();
Presence presence = (Presence) stanza;
if (presence.getType() == Presence.Type.error) {
return;
}
for (ExtensionElement packetExtension : presence.getExtensions()) {
if (packetExtension instanceof VCardUpdate) {
VCardUpdate vCardUpdate = (VCardUpdate) packetExtension;
if (vCardUpdate.isValid() && vCardUpdate.isPhotoReady()) {
try {
onPhotoReady(account, UserJid.from(stanza.getFrom()), vCardUpdate);
} catch (UserJid.UserJidCreateException e) {
LogManager.exception(this, e);
}
}
}
}
}
private void onPhotoReady(final AccountJid account, final UserJid user, VCardUpdate vCardUpdate) {
if (vCardUpdate.isEmpty()) {
setHash(user.getJid(), EMPTY_HASH);
return;
}
final String hash = vCardUpdate.getPhotoHash();
if (bitmaps.containsKey(hash)) {
setHash(user.getJid(), hash);
return;
}
Application.getInstance().runInBackground(new Runnable() {
@Override
public void run() {
loadBitmap(account, user.getJid(), hash);
}
});
}
/**
* Read bitmap in background.
*
*/
private void loadBitmap(final AccountJid account, final Jid jid, final String hash) {
final byte[] value = AvatarStorage.getInstance().read(hash);
final Bitmap bitmap = makeBitmap(value);
Application.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
onBitmapLoaded(account, jid, hash, value, bitmap);
}
});
}
/**
* Update data or request avatar on bitmap load.
*/
private void onBitmapLoaded(AccountJid account, Jid jid,
String hash, byte[] value, Bitmap bitmap) {
if (value == null) {
if (SettingsManager.connectionLoadVCard()) {
VCardManager.getInstance().request(account, jid);
}
} else {
bitmaps.put(hash, bitmap == null ? EMPTY_BITMAP : bitmap);
setHash(jid, hash);
}
}
/**
* @param bitmap
* @return Scaled bitmap to be used for shortcut.
*/
public Bitmap createShortcutBitmap(Bitmap bitmap) {
int size = getLauncherLargeIconSize();
int max = Math.max(bitmap.getWidth(), bitmap.getHeight());
if (max == size) {
return bitmap;
}
double scale = ((double) size) / max;
int width = (int) (bitmap.getWidth() * scale);
int height = (int) (bitmap.getHeight() * scale);
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
private int getLauncherLargeIconSize() {
return HoneycombShortcutHelper.getLauncherLargeIconSize();
}
}