anthony restaino
8 years ago
9 changed files with 314 additions and 224 deletions
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -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); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -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; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
} |
|
Loading…
Reference in new issue