Browse Source

Better asynchronous image loading for BookmarksFragment

Previous AsyncTask would throw a RejectedExecutionException if too many
AsyncTasks got spawned on the thread pool executor. The solution was to
create a custom Executor that properly executed the task and queue it if
necessary. Also switched to using weakreference for the view and set
timeouts on image loading so it can load faster.
master
Anthony Restaino 9 years ago
parent
commit
a71a8c3493
  1. 50
      app/src/main/java/acr/browser/lightning/async/AsyncExecutor.java
  2. 51
      app/src/main/java/acr/browser/lightning/async/ImageDownloadTask.java
  3. 21
      app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java

50
app/src/main/java/acr/browser/lightning/async/AsyncExecutor.java

@ -0,0 +1,50 @@
package acr.browser.lightning.async;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
/**
* Created 9/27/2015 Anthony Restaino
*/
public class AsyncExecutor implements Executor {
private static final String TAG = AsyncExecutor.class.getSimpleName();
private static AsyncExecutor INSTANCE = new AsyncExecutor();
private Queue<Runnable> mQueue = new ArrayDeque<>(1);
private Executor mExecutor = Executors.newFixedThreadPool(4);
private AsyncExecutor() {}
public static AsyncExecutor getInstance() {
return INSTANCE;
}
public synchronized void notifyThreadFinish() {
if (mQueue.isEmpty()) {
return;
}
Runnable runnable = mQueue.remove();
execute(runnable);
}
@Override
protected void finalize() throws Throwable {
super.finalize();
}
@Override
public void execute(@NonNull Runnable command) {
try {
mExecutor.execute(command);
} catch (RejectedExecutionException ignored) {
mQueue.add(command);
Log.d(TAG, "Thread was enqueued");
}
}
}

51
app/src/main/java/acr/browser/lightning/utils/DownloadImageTask.java → app/src/main/java/acr/browser/lightning/async/ImageDownloadTask.java

@ -1,4 +1,4 @@
package acr.browser.lightning.utils; package acr.browser.lightning.async;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
@ -11,33 +11,35 @@ import android.widget.ImageView;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants; import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.database.HistoryItem; import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.Utils;
/** public class ImageDownloadTask extends AsyncTask<Void, Void, Bitmap> {
* Created by Stefano Pacifici on 25/08/15.
*/
public class DownloadImageTask extends AsyncTask<Void, Void, Bitmap> {
private final ImageView bmImage; private static final String TAG = ImageDownloadTask.class.getSimpleName();
private static final File mCacheDir = BrowserApp.getAppContext().getCacheDir();
private final WeakReference<ImageView> bmImage;
private final HistoryItem mWeb; private final HistoryItem mWeb;
private final File mCacheDir;
private final String mUrl; private final String mUrl;
private final Bitmap mDefaultBitmap; private final Bitmap mDefaultBitmap;
public DownloadImageTask(@NonNull ImageView bmImage, @NonNull HistoryItem web, public ImageDownloadTask(@NonNull ImageView bmImage, @NonNull HistoryItem web, @NonNull Bitmap defaultBitmap) {
@NonNull Bitmap defaultBitmap) { // Set a tag on the ImageView so we know if the view
this.bmImage = bmImage; // has gone out of scope and should not be used
bmImage.setTag(web.getUrl().hashCode());
this.bmImage = new WeakReference<>(bmImage);
this.mWeb = web; this.mWeb = web;
this.mCacheDir = BrowserApp.getAppContext().getCacheDir();
this.mUrl = web.getUrl(); this.mUrl = web.getUrl();
this.mDefaultBitmap = defaultBitmap; this.mDefaultBitmap = defaultBitmap;
} }
@Override
protected Bitmap doInBackground(Void... params) { protected Bitmap doInBackground(Void... params) {
Bitmap mIcon = null; Bitmap mIcon = null;
// unique path for each url that is bookmarked. // unique path for each url that is bookmarked.
@ -60,6 +62,8 @@ public class DownloadImageTask extends AsyncTask<Void, Void, Bitmap> {
final URL urlDownload = new URL(urlDisplay); final URL urlDownload = new URL(urlDisplay);
final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection(); final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection();
connection.setDoInput(true); connection.setDoInput(true);
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
connection.connect(); connection.connect();
in = connection.getInputStream(); in = connection.getInputStream();
@ -74,8 +78,8 @@ public class DownloadImageTask extends AsyncTask<Void, Void, Bitmap> {
Log.d(Constants.TAG, "Downloaded: " + urlDisplay); Log.d(Constants.TAG, "Downloaded: " + urlDisplay);
} }
} catch (Exception e) { } catch (Exception ignored) {
e.printStackTrace(); Log.d(TAG, "Could not download: " + urlDisplay);
} finally { } finally {
Utils.close(in); Utils.close(in);
Utils.close(fos); Utils.close(fos);
@ -89,10 +93,11 @@ public class DownloadImageTask extends AsyncTask<Void, Void, Bitmap> {
FileOutputStream fos = null; FileOutputStream fos = null;
try { try {
// if not, download it... // if not, download it...
final URL urlDownload = new URL("https://www.google.com/s2/favicons?domain_url=" final URL urlDownload = new URL("https://www.google.com/s2/favicons?domain_url=" + uri.toString());
+ uri.toString());
final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection(); final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection();
connection.setDoInput(true); connection.setDoInput(true);
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
connection.connect(); connection.connect();
in = connection.getInputStream(); in = connection.getInputStream();
@ -107,7 +112,7 @@ public class DownloadImageTask extends AsyncTask<Void, Void, Bitmap> {
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); Log.d(TAG, "Could not download Google favicon");
} finally { } finally {
Utils.close(in); Utils.close(in);
Utils.close(fos); Utils.close(fos);
@ -120,10 +125,16 @@ public class DownloadImageTask extends AsyncTask<Void, Void, Bitmap> {
} }
} }
protected void onPostExecute(Bitmap result) { @Override
final Bitmap fav = Utils.padFavicon(result); protected void onPostExecute(Bitmap bitmap) {
bmImage.setImageBitmap(fav); super.onPostExecute(bitmap);
AsyncExecutor.getInstance().notifyThreadFinish();
final Bitmap fav = Utils.padFavicon(bitmap);
ImageView view = bmImage.get();
if (view != null && view.getTag().equals(mWeb.getUrl().hashCode())) {
view.setImageBitmap(fav);
}
mWeb.setBitmap(fav); mWeb.setBitmap(fav);
// notifyBookmarkDataSetChanged();
} }
} }

21
app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java

@ -3,7 +3,6 @@ package acr.browser.lightning.fragment;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.IdRes; import android.support.annotation.IdRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -38,13 +37,14 @@ import javax.inject.Inject;
import acr.browser.lightning.R; import acr.browser.lightning.R;
import acr.browser.lightning.activity.BrowserActivity; import acr.browser.lightning.activity.BrowserActivity;
import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.async.AsyncExecutor;
import acr.browser.lightning.bus.BookmarkEvents; import acr.browser.lightning.bus.BookmarkEvents;
import acr.browser.lightning.bus.BrowserEvents; import acr.browser.lightning.bus.BrowserEvents;
import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.BookmarkManager;
import acr.browser.lightning.database.HistoryItem; import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.dialog.BookmarksDialogBuilder; import acr.browser.lightning.dialog.BookmarksDialogBuilder;
import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.DownloadImageTask; import acr.browser.lightning.async.ImageDownloadTask;
import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.ThemeUtils;
/** /**
@ -103,8 +103,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final HistoryItem item = mBookmarks.get(position); final HistoryItem item = mBookmarks.get(position);
if (item.isFolder()) { if (item.isFolder()) {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), true);
true);
} else { } else {
mEventBus.post(new BookmarkEvents.Clicked(item)); mEventBus.post(new BookmarkEvents.Clicked(item));
} }
@ -139,8 +138,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
backView.setOnClickListener(new View.OnClickListener() { backView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (mBookmarkManager == null) if (mBookmarkManager == null) return;
return;
if (!mBookmarkManager.isRootFolder()) { if (!mBookmarkManager.isRootFolder()) {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true); setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true);
} }
@ -186,8 +184,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
mBookmarks.add(item); mBookmarks.add(item);
Collections.sort(mBookmarks, new BookmarkManager.SortIgnoreCase()); Collections.sort(mBookmarks, new BookmarkManager.SortIgnoreCase());
mBookmarkAdapter.notifyDataSetChanged(); mBookmarkAdapter.notifyDataSetChanged();
mEventBus mEventBus.post(new BookmarkEvents.Added(item));
.post(new BookmarkEvents.Added(item));
updateBookmarkIndicator(event.url); updateBookmarkIndicator(event.url);
} }
} }
@ -345,19 +342,19 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
HistoryItem web = mBookmarks.get(position); HistoryItem web = mBookmarks.get(position);
holder.txtTitle.setText(web.getTitle()); holder.txtTitle.setText(web.getTitle());
holder.favicon.setImageBitmap(mWebpageBitmap);
if (web.isFolder()) { if (web.isFolder()) {
holder.favicon.setImageBitmap(mFolderBitmap); holder.favicon.setImageBitmap(mFolderBitmap);
} else if (web.getBitmap() == null) { } else if (web.getBitmap() == null) {
new DownloadImageTask(holder.favicon, web, mWebpageBitmap) holder.favicon.setImageBitmap(mWebpageBitmap);
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new ImageDownloadTask(holder.favicon, web, mWebpageBitmap)
.executeOnExecutor(AsyncExecutor.getInstance());
} else { } else {
holder.favicon.setImageBitmap(web.getBitmap()); holder.favicon.setImageBitmap(web.getBitmap());
} }
return row; return row;
} }
class BookmarkViewHolder { private class BookmarkViewHolder {
TextView txtTitle; TextView txtTitle;
ImageView favicon; ImageView favicon;
} }

Loading…
Cancel
Save