package com.gh4a.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import org.eclipse.egit.github.core.User;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v4.util.LruCache;
import android.support.v4.util.SparseArrayCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.widget.ImageView;
public class AvatarHandler {
private static final String TAG = "GravatarHandler";
private static final int MAX_CACHED_IMAGE_SIZE = 60; /* dp - maximum gravatar view size used */
private static LruCache<Integer, Bitmap> sCache;
private static int sNextRequestId = 1;
private static class Request {
int id;
String url;
ArrayList<ViewDelegate> views;
}
private static final SparseArrayCompat<Request> sRequests = new SparseArrayCompat<>();
private static int sMaxImageSizePx = -1;
private static final int MSG_LOAD = 1;
private static final int MSG_LOADED = 2;
private static final int MSG_DESTROY = 3;
private static HandlerThread sWorkerThread = null;
private static Handler sWorkerHandler = null;
private static final Handler sHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOADED:
processResult(msg.arg1, (Bitmap) msg.obj);
if (sRequests.size() == 0) {
sendEmptyMessageDelayed(MSG_DESTROY, 3000);
}
break;
case MSG_DESTROY:
shutdownWorker();
break;
}
}
private void processResult(int requestId, Bitmap bitmap) {
final Request request = sRequests.get(requestId);
if (request != null && bitmap != null) {
sCache.put(request.id, bitmap);
for (ViewDelegate view : request.views) {
applyAvatarToView(view, bitmap);
}
}
sRequests.delete(requestId);
}
};
public static void assignAvatar(ImageView view, User user) {
if (user == null) {
assignAvatar(view, null, 0, null);
return;
}
assignAvatar(view, user.getLogin(), user.getId(), user.getAvatarUrl());
}
public static void assignAvatar(ImageView view, String userName, int userId, String url) {
assignAvatarInternal(new ImageViewDelegate(view), userName, userId, url);
}
public static void assignAvatar(Context context, MenuItem item,
String userName, int userId, String url) {
assignAvatarInternal(new MenuItemDelegate(context, item), userName, userId, url);
}
public static void assignAvatarInternal(ViewDelegate view,
String userName, int userId, String url) {
removeOldRequest(view);
if (sCache == null) {
initialize(view.getContext());
}
Bitmap bitmap = sCache.get(userId);
if (bitmap != null) {
applyAvatarToView(view, bitmap);
return;
}
view.setDrawable(new DefaultAvatarDrawable(view.getContext(), userName));
if (userId <= 0) {
return;
}
Request request = getRequestForId(userId);
if (request != null) {
request.views.add(view);
return;
}
int requestId = sNextRequestId++;
request = new Request();
request.id = userId;
request.url = makeUrl(url, userId);
request.views = new ArrayList<>();
request.views.add(view);
sRequests.put(requestId, request);
sHandler.removeMessages(MSG_DESTROY);
if (sWorkerThread == null) {
sWorkerThread = new HandlerThread("GravatarLoader");
sWorkerThread.start();
sWorkerHandler = new WorkerHandler(sWorkerThread.getLooper());
}
Message msg = sWorkerHandler.obtainMessage(MSG_LOAD, requestId, 0, request.url);
msg.sendToTarget();
}
private static void initialize(Context context) {
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 10% of the available memory or 1MB for the cache, whatever is larger
final int limit = Math.max(maxMemory / 10, 1024);
sCache = new LruCache<Integer, Bitmap>(limit) {
@Override
protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
oldValue.recycle();
}
@Override
protected int sizeOf(Integer key, Bitmap value) {
final long sizeInBytes;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
sizeInBytes = value.getAllocationByteCount();
} else {
sizeInBytes = value.getRowBytes() * value.getHeight();
}
return (int) (sizeInBytes / 1024);
}
};
Resources res = context.getResources();
sMaxImageSizePx = Math.round(res.getDisplayMetrics().density * MAX_CACHED_IMAGE_SIZE);
}
private static String makeUrl(String url, int userId) {
if (url == null) {
url = "https://avatars.githubusercontent.com/u/" + userId;
}
return Uri.parse(url).buildUpon()
.appendQueryParameter("s", String.valueOf(sMaxImageSizePx))
.toString();
}
private static void applyAvatarToView(ViewDelegate view, Bitmap avatar) {
Resources res = view.getContext().getResources();
RoundedBitmapDrawable d = RoundedBitmapDrawableFactory.create(res, avatar);
d.setCornerRadius(Math.max(avatar.getWidth() / 2, avatar.getHeight() / 2));
d.setAntiAlias(true);
Drawable old = view.getDrawable();
if (old instanceof DefaultAvatarDrawable) {
TransitionDrawable transition = new TransitionDrawable(new Drawable[] { old, d });
transition.setCrossFadeEnabled(true);
transition.startTransition(res.getInteger(android.R.integer.config_shortAnimTime));
view.setDrawable(transition);
} else {
view.setDrawable(d);
}
}
private static Request getRequestForId(int id) {
int count = sRequests.size();
for (int i = 0; i < count; i++) {
Request request = sRequests.valueAt(i);
if (request.id == id) {
return request;
}
}
return null;
}
private static void removeOldRequest(ViewDelegate view) {
int count = sRequests.size();
for (int i = 0; i < count; i++) {
Request request = sRequests.valueAt(i);
if (request.views.remove(view)) {
if (request.views.isEmpty()) {
if (sWorkerHandler != null) {
sWorkerHandler.removeMessages(MSG_LOAD, request.url);
}
sRequests.delete(sRequests.keyAt(i));
}
return;
}
}
}
private static Bitmap fetchBitmap(String url) throws IOException {
URL realUrl = new URL(url);
InputStream input = realUrl.openStream();
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
byte[] data = output.toByteArray();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, options);
options.inJustDecodeBounds = false;
final int widthRatio = options.outWidth / sMaxImageSizePx;
final int heightRatio = options.outHeight / sMaxImageSizePx;
options.inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
Bitmap unscaled = BitmapFactory.decodeByteArray(data, 0, data.length, options);
if (unscaled == null) {
return null;
}
// We'll scale the image to the desired density
unscaled.setDensity(0);
float widthScale = (float) sMaxImageSizePx / (float) unscaled.getWidth();
float heightScale = (float) sMaxImageSizePx / (float) unscaled.getHeight();
float scaleFactor = Math.min(1, Math.min(widthScale, heightScale));
Bitmap scaled = Bitmap.createScaledBitmap(unscaled,
(int) (scaleFactor * unscaled.getWidth()),
(int) (scaleFactor * unscaled.getHeight()), true);
if (scaled != unscaled) {
unscaled.recycle();
}
return scaled;
}
private static void shutdownWorker() {
if (sWorkerThread != null) {
sWorkerThread.getLooper().quit();
sWorkerHandler = null;
sWorkerThread = null;
}
}
private static class WorkerHandler extends Handler {
public WorkerHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOAD:
String url = (String) msg.obj;
Bitmap bitmap = null;
try {
bitmap = fetchBitmap(url);
} catch (IOException e) {
Log.e(TAG, "Couldn't fetch gravatar from URL " + url, e);
}
sHandler.obtainMessage(MSG_LOADED, msg.arg1, 0, bitmap).sendToTarget();
break;
}
}
}
public static class DefaultAvatarDrawable extends Drawable {
private static final @ColorInt int[] COLOR_PALETTE = {
0xffdb4437, 0xffe91e63, 0xff9c27b0, 0xff673ab7,
0xff3f51b5, 0xff4285f4, 0xff039be5, 0xff0097a7,
0xff009688, 0xff0f9d58, 0xff689f38, 0xffef6c00,
0xffff5722, 0xff757575
};
private static final float LETTER_TO_TILE_RATIO = 0.67f;
private final Paint mPaint;
private final @ColorInt int mColor;
private final char[] mLetter = new char[1];
private static final Rect sRect = new Rect();
public DefaultAvatarDrawable(Context context, String userName) {
mPaint = new Paint();
mPaint.setTypeface(TypefaceCache.getTypeface(context, TypefaceCache.TF_MEDIUM));
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setAntiAlias(true);
final int colorIndex;
if (TextUtils.isEmpty(userName)) {
mLetter[0] = '?';
colorIndex = (int) (Math.random() * COLOR_PALETTE.length);
} else {
mLetter[0] = Character.toUpperCase(userName.charAt(0));
colorIndex = Math.abs(userName.hashCode()) % COLOR_PALETTE.length;
}
mColor = COLOR_PALETTE[colorIndex];
}
@Override
public void draw(@NonNull final Canvas canvas) {
final Rect bounds = getBounds();
if (!isVisible() || bounds.isEmpty()) {
return;
}
mPaint.setColor(mColor);
final int minDimension = Math.min(bounds.width(), bounds.height());
canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, mPaint);
mPaint.setTextSize(LETTER_TO_TILE_RATIO * minDimension);
mPaint.getTextBounds(mLetter, 0, 1, sRect);
mPaint.setColor(Color.WHITE);
canvas.drawText(mLetter, 0, 1, bounds.centerX(),
bounds.centerY() - sRect.exactCenterY(),
mPaint);
}
@Override
public void setAlpha(final int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(final ColorFilter cf) {
mPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return android.graphics.PixelFormat.OPAQUE;
}
}
private interface ViewDelegate {
Context getContext();
Drawable getDrawable();
void setDrawable(Drawable d);
}
private static class ImageViewDelegate implements ViewDelegate {
private ImageView mView;
public ImageViewDelegate(ImageView view) {
mView = view;
}
@Override
public Context getContext() {
return mView.getContext();
}
@Override
public Drawable getDrawable() {
return mView.getDrawable();
}
@Override
public void setDrawable(Drawable d) {
mView.setImageDrawable(d);
}
@Override
public boolean equals(Object obj) {
return obj instanceof ImageViewDelegate && ((ImageViewDelegate) obj).mView == mView;
}
}
private static class MenuItemDelegate implements ViewDelegate {
private Context mContext;
private MenuItem mItem;
public MenuItemDelegate(Context context, MenuItem item) {
mContext = context;
mItem = item;
}
@Override
public Context getContext() {
return mContext;
}
@Override
public Drawable getDrawable() {
return mItem.getIcon();
}
@Override
public void setDrawable(Drawable d) {
mItem.setIcon(d);
}
@Override
public boolean equals(Object obj) {
return obj instanceof MenuItemDelegate && ((MenuItemDelegate) obj).mItem == mItem;
}
}
}