Browse Source

Cleaning up image request logic

master
anthony restaino 8 years ago
parent
commit
cca39aa3d2
  1. 3
      app/src/main/java/acr/browser/lightning/app/AppComponent.java
  2. 139
      app/src/main/java/acr/browser/lightning/favicon/FaviconModel.java
  3. 33
      app/src/main/java/acr/browser/lightning/favicon/FaviconUtils.java
  4. 81
      app/src/main/java/acr/browser/lightning/favicon/ImageFetcher.java
  5. 11
      app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java
  6. 54
      app/src/main/java/acr/browser/lightning/utils/Utils.java
  7. 42
      app/src/main/java/acr/browser/lightning/view/IconCacheTask.java
  8. 152
      app/src/main/java/acr/browser/lightning/view/ImageDownloader.java
  9. 23
      app/src/main/java/acr/browser/lightning/view/LightningChromeClient.java

3
app/src/main/java/acr/browser/lightning/app/AppComponent.java

@ -7,7 +7,6 @@ import acr.browser.lightning.activity.ReadingActivity; @@ -7,7 +7,6 @@ import acr.browser.lightning.activity.ReadingActivity;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.activity.ThemableBrowserActivity;
import acr.browser.lightning.activity.ThemableSettingsActivity;
import acr.browser.lightning.view.ImageDownloader;
import acr.browser.lightning.browser.BrowserPresenter;
import acr.browser.lightning.constant.BookmarkPage;
import acr.browser.lightning.constant.HistoryPage;
@ -77,6 +76,4 @@ public interface AppComponent { @@ -77,6 +76,4 @@ public interface AppComponent {
void inject(SuggestionsAdapter suggestionsAdapter);
void inject(ImageDownloader imageDownloader);
}

139
app/src/main/java/acr/browser/lightning/favicon/FaviconModel.java

@ -0,0 +1,139 @@ @@ -0,0 +1,139 @@
package acr.browser.lightning.favicon;
import android.app.Application;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import com.anthonycr.bonsai.Completable;
import com.anthonycr.bonsai.CompletableAction;
import com.anthonycr.bonsai.CompletableSubscriber;
import com.anthonycr.bonsai.Single;
import com.anthonycr.bonsai.SingleAction;
import com.anthonycr.bonsai.SingleSubscriber;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.utils.Utils;
/**
* Reactive model that can fetch favicons
* from URLs and also cache them.
*/
public class FaviconModel {
private static final String TAG = "FaviconModel";
private final ImageFetcher mImageFetcher;
public FaviconModel() {
mImageFetcher = new ImageFetcher();
}
@NonNull
private static File createFaviconCacheFile(@NonNull Application app, @NonNull Uri uri) {
FaviconUtils.assertUriSafe(uri);
String hash = String.valueOf(uri.getHost().hashCode());
return new File(app.getCacheDir(), hash + ".png");
}
@NonNull
public Single<Bitmap> faviconForUrl(@NonNull final String url,
@NonNull final Bitmap defaultFavicon,
final boolean allowGoogleService) {
return Single.create(new SingleAction<Bitmap>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<Bitmap> subscriber) {
Uri uri = FaviconUtils.safeUri(url);
if (uri == null) {
Bitmap newFavicon = Utils.padFavicon(defaultFavicon);
subscriber.onItem(newFavicon);
subscriber.onComplete();
return;
}
Application app = BrowserApp.getApplication();
File faviconCacheFile = createFaviconCacheFile(app, uri);
Bitmap favicon = null;
if (faviconCacheFile.exists()) {
favicon = mImageFetcher.retrieveFaviconFromCache(faviconCacheFile);
}
if (favicon == null) {
favicon = mImageFetcher.retrieveBitmapFromDomain(uri);
} else {
Bitmap newFavicon = Utils.padFavicon(favicon);
subscriber.onItem(newFavicon);
subscriber.onComplete();
return;
}
if (favicon == null && allowGoogleService) {
favicon = mImageFetcher.retrieveBitmapFromGoogle(uri);
}
if (favicon != null) {
cacheFaviconForUrl(favicon, url).subscribe();
}
if (favicon == null) {
favicon = defaultFavicon;
}
Bitmap newFavicon = Utils.padFavicon(favicon);
subscriber.onItem(newFavicon);
subscriber.onComplete();
}
});
}
@NonNull
public Completable cacheFaviconForUrl(@NonNull final Bitmap favicon,
@NonNull final String url) {
return Completable.create(new CompletableAction() {
@Override
public void onSubscribe(@NonNull CompletableSubscriber subscriber) {
Uri uri = FaviconUtils.safeUri(url);
if (uri == null) {
subscriber.onComplete();
return;
}
Application app = BrowserApp.getApplication();
Log.d(TAG, "Caching icon for " + uri.getHost());
FileOutputStream fos = null;
try {
File image = createFaviconCacheFile(app, uri);
fos = new FileOutputStream(image);
favicon.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
} catch (IOException e) {
Log.e(TAG, "Unable to cache favicon", e);
} finally {
Utils.close(fos);
}
}
});
}
}

33
app/src/main/java/acr/browser/lightning/favicon/FaviconUtils.java

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
package acr.browser.lightning.favicon;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
/**
* Created by anthonycr on 4/13/17.
*/
class FaviconUtils {
@Nullable
static Uri safeUri(@NonNull String url) {
if (TextUtils.isEmpty(url)) {
return null;
}
Uri uri = Uri.parse(url);
if (uri.getHost() == null || uri.getScheme() == null) {
return null;
}
return uri;
}
static void assertUriSafe(@Nullable Uri uri) {
if (uri == null || TextUtils.isEmpty(uri.getScheme()) || TextUtils.isEmpty(uri.getHost())) {
throw new RuntimeException("Unsafe uri provided");
}
}
}

81
app/src/main/java/acr/browser/lightning/favicon/ImageFetcher.java

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
package acr.browser.lightning.favicon;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.File;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import acr.browser.lightning.utils.Utils;
/**
* An image fetcher that creates image
* loading requests on demand.
*/
class ImageFetcher {
private static final String TAG = "ImageFetcher";
@NonNull private final BitmapFactory.Options mLoaderOptions = new BitmapFactory.Options();
ImageFetcher() {
}
@Nullable
Bitmap retrieveFaviconFromCache(@NonNull File cacheFile) {
return BitmapFactory.decodeFile(cacheFile.getPath(), mLoaderOptions);
}
@Nullable
Bitmap retrieveBitmapFromDomain(@NonNull Uri uri) {
FaviconUtils.assertUriSafe(uri);
String faviconUrlGuess = uri.getScheme() + "://" + uri.getHost() + "/favicon.ico";
return retrieveBitmapFromUrl(faviconUrlGuess);
}
@Nullable
Bitmap retrieveBitmapFromGoogle(@NonNull Uri uri) {
FaviconUtils.assertUriSafe(uri);
String googleFaviconUrl = "https://www.google.com/s2/favicons?domain_url=" + uri.toString();
return retrieveBitmapFromUrl(googleFaviconUrl);
}
@Nullable
private Bitmap retrieveBitmapFromUrl(@NonNull String url) {
InputStream in = null;
Bitmap icon = null;
try {
final URL urlDownload = new URL(url);
final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection();
connection.setDoInput(true);
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
connection.connect();
in = connection.getInputStream();
if (in != null) {
icon = BitmapFactory.decodeStream(in, null, mLoaderOptions);
}
} catch (Exception ignored) {
Log.d(TAG, "Could not download icon from: " + url);
} finally {
Utils.close(in);
}
return icon;
}
}

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

@ -45,7 +45,7 @@ import acr.browser.lightning.R; @@ -45,7 +45,7 @@ import acr.browser.lightning.R;
import acr.browser.lightning.activity.ReadingActivity;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.view.ImageDownloader;
import acr.browser.lightning.favicon.FaviconModel;
import acr.browser.lightning.browser.BookmarksView;
import acr.browser.lightning.bus.BookmarkEvents;
import acr.browser.lightning.constant.Constants;
@ -74,7 +74,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @@ -74,7 +74,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
@Inject PreferenceManager mPreferenceManager;
private ImageDownloader mImageDownloader;
private FaviconModel mFaviconModel;
private TabsManager mTabsManager;
@ -128,7 +128,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @@ -128,7 +128,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
mIconColor = darkTheme ? ThemeUtils.getIconDarkThemeColor(context) :
ThemeUtils.getIconLightThemeColor(context);
mImageDownloader = new ImageDownloader(mWebpageBitmap);
mFaviconModel = new FaviconModel();
}
private TabsManager getTabsManager() {
@ -228,7 +228,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @@ -228,7 +228,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
mIconColor = darkTheme ? ThemeUtils.getIconDarkThemeColor(activity) :
ThemeUtils.getIconLightThemeColor(activity);
mImageDownloader = new ImageDownloader(mWebpageBitmap);
mFaviconModel = new FaviconModel();
}
private void updateBookmarkIndicator(final String url) {
@ -405,7 +405,8 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @@ -405,7 +405,8 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
final String url = web.getUrl();
final WeakReference<ImageView> imageViewReference = new WeakReference<>(holder.favicon);
mImageDownloader.newImageRequest(url)
mFaviconModel.faviconForUrl(url, mWebpageBitmap, true)
.subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<Bitmap>() {

54
app/src/main/java/acr/browser/lightning/utils/Utils.java

@ -14,6 +14,7 @@ import android.content.pm.PackageManager; @@ -14,6 +14,7 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
@ -74,7 +75,7 @@ public final class Utils { @@ -74,7 +75,7 @@ public final class Utils {
public static void downloadFile(final Activity activity, final PreferenceManager manager, final String url,
final String userAgent, final String contentDisposition) {
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(activity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionsResultAction() {
Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionsResultAction() {
@Override
public void onGranted() {
String fileName = URLUtil.guessFileName(url, null, null);
@ -124,13 +125,13 @@ public final class Utils { @@ -124,13 +125,13 @@ public final class Utils {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title);
builder.setMessage(message)
.setCancelable(true)
.setPositiveButton(activity.getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
}
});
.setCancelable(true)
.setPositiveButton(activity.getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
}
});
AlertDialog alert = builder.create();
alert.show();
BrowserDialog.setDialogSize(activity, alert);
@ -259,7 +260,7 @@ public final class Utils { @@ -259,7 +260,7 @@ public final class Utils {
int padding = Utils.dpToPx(4);
Bitmap paddedBitmap = Bitmap.createBitmap(bitmap.getWidth() + padding, bitmap.getHeight()
+ padding, Bitmap.Config.ARGB_8888);
+ padding, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(paddedBitmap);
canvas.drawARGB(0x00, 0x00, 0x00, 0x00); // this represents white color
@ -303,10 +304,10 @@ public final class Utils { @@ -303,10 +304,10 @@ public final class Utils {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + '_';
File storageDir = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
return File.createTempFile(imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
".jpg", /* suffix */
storageDir /* directory */
);
}
@ -380,9 +381,9 @@ public final class Utils { @@ -380,9 +381,9 @@ public final class Utils {
paint.setDither(true);
if (withShader) {
paint.setShader(new LinearGradient(0, 0.9f * canvas.getHeight(),
0, canvas.getHeight(),
color, mixTwoColors(Color.BLACK, color, 0.5f),
Shader.TileMode.CLAMP));
0, canvas.getHeight(),
color, mixTwoColors(Color.BLACK, color, 0.5f),
Shader.TileMode.CLAMP));
} else {
paint.setShader(null);
}
@ -431,4 +432,27 @@ public final class Utils { @@ -431,4 +432,27 @@ public final class Utils {
Utils.showSnackbar(activity, R.string.message_added_to_homescreen);
}
public static int calculateInSampleSize(@NonNull BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}

42
app/src/main/java/acr/browser/lightning/view/IconCacheTask.java

@ -1,42 +0,0 @@ @@ -1,42 +0,0 @@
package acr.browser.lightning.view;
import android.app.Application;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.utils.Utils;
class IconCacheTask implements Runnable {
private final Uri uri;
private final Bitmap icon;
private final Application app;
public IconCacheTask(final Uri uri, final Bitmap icon, final Application app) {
this.uri = uri;
this.icon = icon;
this.app = app;
}
@Override
public void run() {
String hash = String.valueOf(uri.getHost().hashCode());
Log.d(Constants.TAG, "Caching icon for " + uri.getHost());
FileOutputStream fos = null;
try {
File image = new File(app.getCacheDir(), hash + ".png");
fos = new FileOutputStream(image);
icon.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.close(fos);
}
}
}

152
app/src/main/java/acr/browser/lightning/view/ImageDownloader.java

@ -1,152 +0,0 @@ @@ -1,152 +0,0 @@
package acr.browser.lightning.view;
import android.app.Application;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.anthonycr.bonsai.Single;
import com.anthonycr.bonsai.SingleAction;
import com.anthonycr.bonsai.SingleSubscriber;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.inject.Inject;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.utils.Utils;
/**
* An ImageDownloader that creates image
* loading requests on demand.
*/
public class ImageDownloader {
private static final String TAG = "ImageDownloader";
@Inject Application mApp;
@NonNull private final Bitmap mDefaultBitmap;
@NonNull private final BitmapFactory.Options mLoaderOptions = new BitmapFactory.Options();
public ImageDownloader(@NonNull Bitmap defaultBitmap) {
BrowserApp.getAppComponent().inject(this);
mDefaultBitmap = defaultBitmap;
}
/**
* Creates a new image request for the given url.
* Emits the bitmap associated with that url, or
* the default bitmap if none was found.
*
* @param url the url for which to retrieve the bitmap.
* @return a single that emits the bitmap that was found.
*/
@NonNull
public Single<Bitmap> newImageRequest(@Nullable final String url) {
return Single.create(new SingleAction<Bitmap>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<Bitmap> subscriber) {
Bitmap favicon = retrieveFaviconForUrl(url);
Bitmap paddedFavicon = Utils.padFavicon(favicon);
subscriber.onItem(paddedFavicon);
subscriber.onComplete();
}
});
}
@NonNull
private Bitmap retrieveFaviconForUrl(@Nullable String url) {
// unique path for each url that is bookmarked.
if (url == null) {
return mDefaultBitmap;
}
Bitmap icon;
File cache = mApp.getCacheDir();
Uri uri = Uri.parse(url);
if (uri.getHost() == null || uri.getScheme() == null || Constants.FILE.startsWith(uri.getScheme())) {
return mDefaultBitmap;
}
String hash = String.valueOf(uri.getHost().hashCode());
File image = new File(cache, hash + ".png");
String urlDisplay = uri.getScheme() + "://" + uri.getHost() + "/favicon.ico";
if (image.exists()) {
// If image exists, pull it from the cache
icon = BitmapFactory.decodeFile(image.getPath());
} else {
// Otherwise, load it from network
icon = retrieveBitmapFromUrl(urlDisplay);
}
if (icon == null) {
String googleFaviconUrl = "https://www.google.com/s2/favicons?domain_url=" + uri.toString();
icon = retrieveBitmapFromUrl(googleFaviconUrl);
}
if (icon == null) {
return mDefaultBitmap;
} else {
cacheBitmap(image, icon);
return icon;
}
}
private void cacheBitmap(@NonNull File cacheFile, @NonNull Bitmap imageToCache) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(cacheFile);
imageToCache.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
} catch (IOException e) {
Log.e(TAG, "Could not cache icon");
} finally {
Utils.close(fos);
}
}
@Nullable
private Bitmap retrieveBitmapFromUrl(@NonNull String url) {
InputStream in = null;
Bitmap icon = null;
try {
final URL urlDownload = new URL(url);
final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection();
connection.setDoInput(true);
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
connection.connect();
in = connection.getInputStream();
if (in != null) {
icon = BitmapFactory.decodeStream(in, null, mLoaderOptions);
}
} catch (Exception ignored) {
Log.d(TAG, "Could not download icon from: " + url);
} finally {
Utils.close(in);
}
return icon;
}
}

23
app/src/main/java/acr/browser/lightning/view/LightningChromeClient.java

@ -2,7 +2,6 @@ package acr.browser.lightning.view; @@ -2,7 +2,6 @@ package acr.browser.lightning.view;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Bitmap;
@ -19,13 +18,14 @@ import android.webkit.ValueCallback; @@ -19,13 +18,14 @@ import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import com.anthonycr.bonsai.Schedulers;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.dialog.BrowserDialog;
import acr.browser.lightning.favicon.FaviconModel;
import acr.browser.lightning.utils.Preconditions;
class LightningChromeClient extends WebChromeClient {
@ -37,6 +37,7 @@ class LightningChromeClient extends WebChromeClient { @@ -37,6 +37,7 @@ class LightningChromeClient extends WebChromeClient {
@NonNull private final Activity mActivity;
@NonNull private final LightningView mLightningView;
@NonNull private final UIController mUIController;
@NonNull private final FaviconModel mFaviconModel;
LightningChromeClient(@NonNull Activity activity, @NonNull LightningView lightningView) {
Preconditions.checkNonNull(activity);
@ -44,6 +45,7 @@ class LightningChromeClient extends WebChromeClient { @@ -44,6 +45,7 @@ class LightningChromeClient extends WebChromeClient {
mActivity = activity;
mUIController = (UIController) activity;
mLightningView = lightningView;
mFaviconModel = new FaviconModel();
}
@Override
@ -57,7 +59,7 @@ class LightningChromeClient extends WebChromeClient { @@ -57,7 +59,7 @@ class LightningChromeClient extends WebChromeClient {
public void onReceivedIcon(@NonNull WebView view, Bitmap icon) {
mLightningView.getTitleInfo().setFavicon(icon);
mUIController.tabChanged(mLightningView);
cacheFavicon(view.getUrl(), icon, mActivity);
cacheFavicon(view.getUrl(), icon);
}
/**
@ -65,13 +67,20 @@ class LightningChromeClient extends WebChromeClient { @@ -65,13 +67,20 @@ class LightningChromeClient extends WebChromeClient {
*
* @param icon the icon to cache
*/
private static void cacheFavicon(@Nullable final String url, @Nullable final Bitmap icon, @NonNull final Context context) {
if (icon == null || url == null) return;
final Uri uri = Uri.parse(url);
private void cacheFavicon(@Nullable final String url, @Nullable final Bitmap icon) {
if (icon == null || url == null) {
return;
}
Uri uri = Uri.parse(url);
if (uri.getHost() == null) {
return;
}
BrowserApp.getIOThread().execute(new IconCacheTask(uri, icon, BrowserApp.get(context)));
mFaviconModel.cacheFaviconForUrl(icon, url)
.subscribeOn(Schedulers.io())
.subscribe();
}

Loading…
Cancel
Save