diff --git a/app/src/main/java/eu/kanade/mangafeed/data/cache/CoverCache.java b/app/src/main/java/eu/kanade/mangafeed/data/cache/CoverCache.java new file mode 100644 index 000000000..424820445 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/data/cache/CoverCache.java @@ -0,0 +1,129 @@ +package eu.kanade.mangafeed.data.cache; + +import android.content.Context; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.SimpleTarget; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import eu.kanade.mangafeed.util.DiskUtils; + +public class CoverCache { + + private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache"; + + private Context context; + private File cacheDir; + + public CoverCache(Context context) { + this.context = context; + cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY); + createCacheDir(); + } + + private boolean createCacheDir() { + return !cacheDir.exists() && cacheDir.mkdirs(); + } + + // Download the cover with Glide (it can avoid repeating requests) and save the file on this cache + public void save(String cover, LazyHeaders headers) { + GlideUrl url = new GlideUrl(cover, headers); + Glide.with(context) + .load(url) + .downloadOnly(new SimpleTarget() { + @Override + public void onResourceReady(File resource, GlideAnimation anim) { + try { + add(cover, resource); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + // Copy the cover from Glide's cache to this cache + public void add(String key, File source) throws IOException { + File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(key)); + if (dest.exists()) + dest.delete(); + + InputStream in = new FileInputStream(source); + try { + OutputStream out = new FileOutputStream(dest); + try { + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } finally { + out.close(); + } + } finally { + in.close(); + } + } + + // Get the cover from cache + public File get(String key) { + return new File(cacheDir, DiskUtils.hashKeyForDisk(key)); + } + + // Delete the cover from cache + public boolean delete(String key) { + File file = new File(cacheDir, DiskUtils.hashKeyForDisk(key)); + return file.exists() && file.delete(); + } + + // Load the cover from cache or network if it doesn't exist + public void loadOrFetchInto(ImageView imageView, String cover, LazyHeaders headers) { + File localCover = get(cover); + if (localCover.exists()) { + loadLocalInto(context, imageView, localCover); + } else { + loadRemoteInto(context, imageView, cover, headers); + } + } + + // Load the cover from cache + public static void loadLocalInto(Context context, ImageView imageView, String cover) { + File cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY); + File localCover = new File(cacheDir, DiskUtils.hashKeyForDisk(cover)); + if (localCover.exists()) { + loadLocalInto(context, imageView, localCover); + } + } + + // Load the cover from the cache directory into the specified image view + private static void loadLocalInto(Context context, ImageView imageView, File file) { + Glide.with(context) + .load(file) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .into(imageView); + } + + // Load the cover from network into the specified image view. It does NOT save the image in cache + private static void loadRemoteInto(Context context, ImageView imageView, String cover, LazyHeaders headers) { + GlideUrl url = new GlideUrl(cover, headers); + Glide.with(context) + .load(url) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .centerCrop() + .into(imageView); + } + +} diff --git a/app/src/main/java/eu/kanade/mangafeed/data/network/NetworkHelper.java b/app/src/main/java/eu/kanade/mangafeed/data/network/NetworkHelper.java index e5ae5bb5a..2f05e3850 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/network/NetworkHelper.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/network/NetworkHelper.java @@ -16,95 +16,85 @@ import rx.Observable; public final class NetworkHelper { - private OkHttpClient mClient; + private OkHttpClient client; private CookieManager cookieManager; public final CacheControl NULL_CACHE_CONTROL = new CacheControl.Builder().noCache().build(); public final Headers NULL_HEADERS = new Headers.Builder().build(); public NetworkHelper() { - mClient = new OkHttpClient(); + client = new OkHttpClient(); cookieManager = new CookieManager(); cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); - mClient.setCookieHandler(cookieManager); + client.setCookieHandler(cookieManager); } public Observable getResponse(final String url, final Headers headers, final CacheControl cacheControl) { - return Observable.create(subscriber -> { + return Observable.defer(() -> { try { - if (!subscriber.isUnsubscribed()) { - Request request = new Request.Builder() - .url(url) - .cacheControl(cacheControl != null ? cacheControl : NULL_CACHE_CONTROL) - .headers(headers != null ? headers : NULL_HEADERS) - .build(); - subscriber.onNext(mClient.newCall(request).execute()); - } - subscriber.onCompleted(); + Request request = new Request.Builder() + .url(url) + .cacheControl(cacheControl != null ? cacheControl : NULL_CACHE_CONTROL) + .headers(headers != null ? headers : NULL_HEADERS) + .build(); + + return Observable.just(client.newCall(request).execute()); } catch (Throwable e) { - subscriber.onError(e); + return Observable.error(e); } }).retry(3); } public Observable mapResponseToString(final Response response) { - return Observable.create(subscriber -> { + return Observable.defer(() -> { try { - subscriber.onNext(response.body().string()); - subscriber.onCompleted(); + return Observable.just(response.body().string()); } catch (Throwable e) { - subscriber.onError(e); + return Observable.error(e); } }); } public Observable getStringResponse(final String url, final Headers headers, final CacheControl cacheControl) { - return getResponse(url, headers, cacheControl) .flatMap(this::mapResponseToString); } public Observable postData(final String url, final RequestBody formBody, final Headers headers) { - return Observable.create(subscriber -> { + return Observable.defer(() -> { try { - if (!subscriber.isUnsubscribed()) { - Request request = new Request.Builder() - .url(url) - .post(formBody) - .headers(headers != null ? headers : NULL_HEADERS) - .build(); - subscriber.onNext(mClient.newCall(request).execute()); - } - subscriber.onCompleted(); + Request request = new Request.Builder() + .url(url) + .post(formBody) + .headers(headers != null ? headers : NULL_HEADERS) + .build(); + return Observable.just(client.newCall(request).execute()); } catch (Throwable e) { - subscriber.onError(e); + return Observable.error(e); } - }); + }).retry(3); } public Observable getProgressResponse(final String url, final Headers headers, final ProgressListener listener) { - return Observable.create(subscriber -> { + return Observable.defer(() -> { try { - if (!subscriber.isUnsubscribed()) { - Request request = new Request.Builder() - .url(url) - .cacheControl(NULL_CACHE_CONTROL) - .headers(headers != null ? headers : NULL_HEADERS) + Request request = new Request.Builder() + .url(url) + .cacheControl(NULL_CACHE_CONTROL) + .headers(headers != null ? headers : NULL_HEADERS) + .build(); + + OkHttpClient progressClient = client.clone(); + + progressClient.networkInterceptors().add(chain -> { + Response originalResponse = chain.proceed(chain.request()); + return originalResponse.newBuilder() + .body(new ProgressResponseBody(originalResponse.body(), listener)) .build(); - - OkHttpClient progressClient = mClient.clone(); - - progressClient.networkInterceptors().add(chain -> { - Response originalResponse = chain.proceed(chain.request()); - return originalResponse.newBuilder() - .body(new ProgressResponseBody(originalResponse.body(), listener)) - .build(); - }); - subscriber.onNext(progressClient.newCall(request).execute()); - } - subscriber.onCompleted(); + }); + return Observable.just(progressClient.newCall(request).execute()); } catch (Throwable e) { - subscriber.onError(e); + return Observable.error(e); } }).retry(3); } diff --git a/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Kissmanga.java b/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Kissmanga.java index 6ddd43b8c..a18ce7790 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Kissmanga.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Kissmanga.java @@ -151,7 +151,7 @@ public class Kissmanga extends Source { Element infoElement = parsedDocument.select("div.barContent").first(); Element titleElement = infoElement.select("a.bigChar").first(); Element authorElement = infoElement.select("p:has(span:contains(Author:)) > a").first(); - Element genreElement = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").first(); + Elements genreElement = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)"); Elements descriptionElement = infoElement.select("p:has(span:contains(Summary:)) ~ p"); Element thumbnailUrlElement = parsedDocument.select(".rightBox:eq(0) img").first(); diff --git a/app/src/main/java/eu/kanade/mangafeed/injection/module/DataModule.java b/app/src/main/java/eu/kanade/mangafeed/injection/module/DataModule.java index a373c0870..d7fab6df7 100644 --- a/app/src/main/java/eu/kanade/mangafeed/injection/module/DataModule.java +++ b/app/src/main/java/eu/kanade/mangafeed/injection/module/DataModule.java @@ -7,6 +7,7 @@ import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import eu.kanade.mangafeed.data.cache.CacheManager; +import eu.kanade.mangafeed.data.cache.CoverCache; import eu.kanade.mangafeed.data.chaptersync.ChapterSyncManager; import eu.kanade.mangafeed.data.database.DatabaseHelper; import eu.kanade.mangafeed.data.download.DownloadManager; @@ -38,6 +39,12 @@ public class DataModule { return new CacheManager(app); } + @Provides + @Singleton + CoverCache provideCoverCache(Application app) { + return new CoverCache(app); + } + @Provides @Singleton NetworkHelper provideNetworkHelper() { diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java index d8380ce2b..784d3fba0 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java @@ -67,7 +67,7 @@ public class CatalogueAdapter extends ArrayAdapter { Glide.with(fragment) .load(url) - .diskCacheStrategy(DiskCacheStrategy.RESULT) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) .centerCrop() .into(thumbnail); } else { diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java index 89bbd8163..5c6950288 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java @@ -14,6 +14,7 @@ import android.widget.ImageView; import android.widget.ProgressBar; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.GlideUrl; import java.util.List; @@ -182,6 +183,7 @@ public class CatalogueFragment extends BaseRxFragment { Glide.with(this) .load(url) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) .centerCrop() .into(imageView); } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryHolder.java b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryHolder.java index bc7eab9c8..ec81c0f5a 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryHolder.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryHolder.java @@ -4,12 +4,8 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; - -import java.util.Objects; - import eu.kanade.mangafeed.R; +import eu.kanade.mangafeed.data.cache.CoverCache; import eu.kanade.mangafeed.data.database.models.Manga; import uk.co.ribot.easyadapter.ItemViewHolder; import uk.co.ribot.easyadapter.PositionInfo; @@ -43,17 +39,11 @@ public class LibraryHolder extends ItemViewHolder { unreadText.setVisibility(View.GONE); } - String thumbnail; - if (manga.thumbnail_url != null) - thumbnail = manga.thumbnail_url; - else - thumbnail = "http://img1.wikia.nocookie.net/__cb20090524204255/starwars/images/thumb/1/1a/R2d2.jpg/400px-R2d2.jpg"; - - Glide.with(getContext()) - .load(thumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(this.thumbnail); + if (manga.thumbnail_url != null) { + CoverCache.loadLocalInto(getContext(), thumbnail, manga.thumbnail_url); + } else { + thumbnail.setImageResource(android.R.color.transparent); + } } } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoFragment.java index ffcd818af..746a9c899 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoFragment.java @@ -8,9 +8,6 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; - import butterknife.Bind; import butterknife.ButterKnife; import eu.kanade.mangafeed.R; @@ -65,11 +62,9 @@ public class MangaInfoFragment extends BaseRxFragment { setFavoriteText(manga.favorite); - Glide.with(getActivity()) - .load(manga.thumbnail_url) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(mCover); + getPresenter().coverCache.loadOrFetchInto(mCover, + manga.thumbnail_url, getPresenter().source.getGlideHeaders()); + } public void setChapterCount(int count) { diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java index 7503bcdc5..d128e99c6 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java @@ -4,8 +4,11 @@ import android.os.Bundle; import javax.inject.Inject; +import eu.kanade.mangafeed.data.cache.CoverCache; import eu.kanade.mangafeed.data.database.DatabaseHelper; import eu.kanade.mangafeed.data.database.models.Manga; +import eu.kanade.mangafeed.data.source.SourceManager; +import eu.kanade.mangafeed.data.source.base.Source; import eu.kanade.mangafeed.event.ChapterCountEvent; import eu.kanade.mangafeed.ui.base.presenter.BasePresenter; import eu.kanade.mangafeed.util.EventBusHook; @@ -14,8 +17,11 @@ import rx.Observable; public class MangaInfoPresenter extends BasePresenter { @Inject DatabaseHelper db; + @Inject SourceManager sourceManager; + @Inject CoverCache coverCache; private Manga manga; + protected Source source; private int count = -1; private static final int GET_MANGA = 1; @@ -49,6 +55,7 @@ public class MangaInfoPresenter extends BasePresenter { @EventBusHook public void onEventMainThread(Manga manga) { this.manga = manga; + source = sourceManager.get(manga.source); start(GET_MANGA); } @@ -67,7 +74,16 @@ public class MangaInfoPresenter extends BasePresenter { public void toggleFavorite() { manga.favorite = !manga.favorite; + onMangaFavoriteChange(manga.favorite); db.insertManga(manga).executeAsBlocking(); } + private void onMangaFavoriteChange(boolean isFavorite) { + if (isFavorite) { + coverCache.save(manga.thumbnail_url, source.getGlideHeaders()); + } else { + coverCache.delete(manga.thumbnail_url); + } + } + }