/* * Copyright 2012 GitHub Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.mobile.util; import static android.graphics.Bitmap.CompressFormat.PNG; import static android.graphics.Bitmap.Config.ARGB_8888; import static android.view.View.VISIBLE; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; import com.actionbarsherlock.app.ActionBar; import com.github.kevinsawicki.http.HttpRequest; import com.github.mobile.R.drawable; import com.github.mobile.R.id; import com.github.mobile.core.search.SearchUser; import com.google.inject.Inject; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.egit.github.core.CommitUser; import org.eclipse.egit.github.core.Contributor; import org.eclipse.egit.github.core.User; import roboguice.util.RoboAsyncTask; /** * Avatar utilities */ public class AvatarLoader { private static final String TAG = "AvatarLoader"; private static final float CORNER_RADIUS_IN_DIP = 3; private static final int CACHE_SIZE = 75; private static abstract class FetchAvatarTask extends RoboAsyncTask<BitmapDrawable> { private static final Executor EXECUTOR = Executors .newFixedThreadPool(1); private FetchAvatarTask(Context context) { super(context, EXECUTOR); } @Override protected void onException(Exception e) throws RuntimeException { Log.d(TAG, "Avatar load failed", e); } } private final float cornerRadius; private final Map<Object, BitmapDrawable> loaded = new LinkedHashMap<Object, BitmapDrawable>( CACHE_SIZE, 1.0F) { private static final long serialVersionUID = -4191624209581976720L; @Override protected boolean removeEldestEntry( Map.Entry<Object, BitmapDrawable> eldest) { return size() >= CACHE_SIZE; } }; private final Context context; private final File avatarDir; private final Drawable loadingAvatar; private final Options options; /** * Create avatar helper * * @param context */ @Inject public AvatarLoader(final Context context) { this.context = context; loadingAvatar = context.getResources().getDrawable( drawable.gravatar_icon); avatarDir = new File(context.getCacheDir(), "avatars/github.com"); if (!avatarDir.isDirectory()) avatarDir.mkdirs(); float density = context.getResources().getDisplayMetrics().density; cornerRadius = CORNER_RADIUS_IN_DIP * density; options = new Options(); options.inDither = false; options.inPreferredConfig = ARGB_8888; } private BitmapDrawable getImageBy(final String userId, final String filename) { File avatarFile = new File(avatarDir + "/" + userId, filename); if (!avatarFile.exists() || avatarFile.length() == 0) return null; Bitmap bitmap = decode(avatarFile); if (bitmap != null) return new BitmapDrawable(context.getResources(), bitmap); else { avatarFile.delete(); return null; } } private void deleteCachedUserAvatars(final File userAvatarDir) { if (userAvatarDir.isDirectory()) for (File userAvatar : userAvatarDir.listFiles()) userAvatar.delete(); userAvatarDir.delete(); } private Bitmap decode(final File file) { return BitmapFactory.decodeFile(file.getAbsolutePath(), options); } private String getAvatarFilenameForUrl(final String avatarUrl) { return GravatarUtils.getHash(avatarUrl); } /** * Fetch avatar from URL * * @param url * @param cachedAvatarFilename * @return bitmap */ protected BitmapDrawable fetchAvatar(final String url, final String userId, final String cachedAvatarFilename) { File userAvatarDir = new File(avatarDir, userId); deleteCachedUserAvatars(userAvatarDir); userAvatarDir.mkdirs(); File rawAvatar = new File(userAvatarDir, cachedAvatarFilename + "-raw"); HttpRequest request = HttpRequest.get(url); if (request.ok()) request.receive(rawAvatar); if (!rawAvatar.exists() || rawAvatar.length() == 0) return null; Bitmap bitmap = decode(rawAvatar); if (bitmap == null) { rawAvatar.delete(); return null; } bitmap = ImageUtils.roundCorners(bitmap, cornerRadius); if (bitmap == null) { rawAvatar.delete(); return null; } File roundedAvatar = new File(userAvatarDir, cachedAvatarFilename); FileOutputStream output = null; try { output = new FileOutputStream(roundedAvatar); if (bitmap.compress(PNG, 100, output)) return new BitmapDrawable(context.getResources(), bitmap); else return null; } catch (IOException e) { Log.d(TAG, "Exception writing rounded avatar", e); return null; } finally { if (output != null) try { output.close(); } catch (IOException e) { // Ignored } rawAvatar.delete(); } } /** * Sets the logo on the {@link ActionBar} to the user's avatar. * * @param actionBar * @param user * @return this helper */ public AvatarLoader bind(final ActionBar actionBar, final User user) { return bind(actionBar, new AtomicReference<User>(user)); } /** * Sets the logo on the {@link ActionBar} to the user's avatar. * * @param actionBar * @param userReference * @return this helper */ public AvatarLoader bind(final ActionBar actionBar, final AtomicReference<User> userReference) { if (userReference == null) return this; final User user = userReference.get(); final String userId = getId(user); if (userId == null) return this; final String avatarUrl = user.getAvatarUrl(); if (TextUtils.isEmpty(avatarUrl)) return this; BitmapDrawable loadedImage = loaded.get(userId); if (loadedImage != null) { actionBar.setLogo(loadedImage); return this; } new FetchAvatarTask(context) { @Override public BitmapDrawable call() throws Exception { final String avatarFilename = getAvatarFilenameForUrl(getAvatarUrl(user)); final BitmapDrawable image = getImageBy(userId, avatarFilename); if (image != null) return image; else return fetchAvatar(avatarUrl, userId, avatarFilename); } @Override protected void onSuccess(BitmapDrawable image) throws Exception { if (userId.equals(getId(userReference.get()))) actionBar.setLogo(image); } }.execute(); return this; } private AvatarLoader setImage(final Drawable image, final ImageView view) { return setImage(image, view, null); } private AvatarLoader setImage(final Drawable image, final ImageView view, Object tag) { view.setImageDrawable(image); view.setTag(id.iv_avatar, tag); view.setVisibility(VISIBLE); return this; } private String getAvatarUrl(String id) { if (!TextUtils.isEmpty(id)) return "https://secure.gravatar.com/avatar/" + id + "?d=404"; else return null; } private String getAvatarUrl(User user) { String avatarUrl = user.getAvatarUrl(); if (TextUtils.isEmpty(avatarUrl)) { String gravatarId = user.getGravatarId(); if (TextUtils.isEmpty(gravatarId)) gravatarId = GravatarUtils.getHash(user.getEmail()); avatarUrl = getAvatarUrl(gravatarId); } return avatarUrl; } private String getAvatarUrl(CommitUser user) { return getAvatarUrl(GravatarUtils.getHash(user.getEmail())); } private String getId(final User user) { if (user == null) return null; int id = user.getId(); if (id > 0) return Integer.toString(id); String gravatarId = user.getGravatarId(); if (!TextUtils.isEmpty(gravatarId)) return gravatarId; else return GravatarUtils.getHash(user.getEmail()); } /** * Bind view to image at URL * * @param view * @param user * @return this helper */ public AvatarLoader bind(final ImageView view, final User user) { final String userId = getId(user); if (userId == null) return setImage(loadingAvatar, view); final String avatarUrl = getAvatarUrl(user); if (TextUtils.isEmpty(avatarUrl)) return setImage(loadingAvatar, view); BitmapDrawable loadedImage = loaded.get(userId); if (loadedImage != null) return setImage(loadedImage, view); setImage(loadingAvatar, view, userId); fetchAvatarTask(avatarUrl, userId, view).execute(); return this; } /** * Bind view to image at URL * * @param view * @param user * @return this helper */ public AvatarLoader bind(final ImageView view, final CommitUser user) { if (user == null) return setImage(loadingAvatar, view); final String avatarUrl = getAvatarUrl(user); if (TextUtils.isEmpty(avatarUrl)) return setImage(loadingAvatar, view); final String userId = user.getEmail(); BitmapDrawable loadedImage = loaded.get(userId); if (loadedImage != null) return setImage(loadedImage, view); setImage(loadingAvatar, view, userId); fetchAvatarTask(avatarUrl, userId, view).execute(); return this; } /** * Bind view to image at URL * * @param view * @param contributor * @return this helper */ public AvatarLoader bind(final ImageView view, final Contributor contributor) { if (contributor == null) return setImage(loadingAvatar, view); final String avatarUrl = contributor.getAvatarUrl(); if (TextUtils.isEmpty(avatarUrl)) return setImage(loadingAvatar, view); final String contributorId = contributor.getLogin(); BitmapDrawable loadedImage = loaded.get(contributorId); if (loadedImage != null) return setImage(loadedImage, view); setImage(loadingAvatar, view, contributorId); fetchAvatarTask(avatarUrl, contributorId, view).execute(); return this; } /** * Bind view to image at URL * * @param view * @param user * @return this helper */ public AvatarLoader bind(final ImageView view, final SearchUser user) { if (user == null) return setImage(loadingAvatar, view); final String avatarUrl = getAvatarUrl(user.getGravatarId()); if (TextUtils.isEmpty(avatarUrl)) return setImage(loadingAvatar, view); final String userId = user.getId(); BitmapDrawable loadedImage = loaded.get(userId); if (loadedImage != null) return setImage(loadedImage, view); setImage(loadingAvatar, view, userId); fetchAvatarTask(avatarUrl, userId, view).execute(); return this; } private FetchAvatarTask fetchAvatarTask(final String avatarUrl, final String userId, final ImageView view) { return new FetchAvatarTask(context) { @Override public BitmapDrawable call() throws Exception { if (!userId.equals(view.getTag(id.iv_avatar))) return null; final String avatarFilename = getAvatarFilenameForUrl(avatarUrl); final BitmapDrawable image = getImageBy(userId, avatarFilename); if (image != null) return image; else return fetchAvatar(avatarUrl, userId, avatarFilename); } @Override protected void onSuccess(final BitmapDrawable image) throws Exception { if (image == null) return; loaded.put(userId, image); if (userId.equals(view.getTag(id.iv_avatar))) setImage(image, view); } }; } }