diff --git a/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java b/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java index 066e38a6ea..395f30ebff 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/caches/CacheManager.java @@ -8,6 +8,7 @@ import com.bumptech.glide.request.target.Target; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.jakewharton.disklrucache.DiskLruCache; +import com.squareup.okhttp.Response; import java.io.BufferedOutputStream; import java.io.File; @@ -21,6 +22,8 @@ import java.util.concurrent.TimeoutException; import eu.kanade.mangafeed.data.models.Page; import eu.kanade.mangafeed.util.DiskUtils; +import okio.BufferedSink; +import okio.Okio; import rx.Observable; public class CacheManager { @@ -184,5 +187,62 @@ public class CacheManager { return mDiskCache.getDirectory(); } + public boolean isImageInCache(final String imageUrl) { + try { + return mDiskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null; + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + public String getImagePath(final String imageUrl) { + try { + String imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0"; + File file = new File(mDiskCache.getDirectory(), imageName); + return file.getCanonicalPath(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public boolean putImageToDiskCache(final String imageUrl, final Response response) { + DiskLruCache.Editor editor = null; + BufferedSink sink = null; + + try { + String key = DiskUtils.hashKeyForDisk(imageUrl); + editor = mDiskCache.edit(key); + if (editor == null) { + return false; + } + + OutputStream outputStream = new BufferedOutputStream(editor.newOutputStream(0)); + sink = Okio.buffer(Okio.sink(outputStream)); + sink.writeAll(response.body().source()); + sink.flush(); + + mDiskCache.flush(); + editor.commit(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + if (editor != null) { + editor.abortUnlessCommitted(); + } + if (sink != null) { + try { + sink.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return true; + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/data/helpers/NetworkHelper.java b/app/src/main/java/eu/kanade/mangafeed/data/helpers/NetworkHelper.java index 7ce568d620..f3a5ef4d75 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/helpers/NetworkHelper.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/helpers/NetworkHelper.java @@ -3,15 +3,24 @@ package eu.kanade.mangafeed.data.helpers; import com.squareup.okhttp.CacheControl; import com.squareup.okhttp.Headers; +import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; +import java.io.IOException; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.CookieStore; +import eu.kanade.mangafeed.data.models.Page; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; import rx.Observable; public final class NetworkHelper { @@ -82,8 +91,79 @@ public final class NetworkHelper { }); } + public Observable getProgressResponse(final String url, final Headers headers, final Page page) { + return Observable.create(subscriber -> { + try { + if (!subscriber.isUnsubscribed()) { + Request request = new Request.Builder() + .url(url) + .cacheControl(NULL_CACHE_CONTROL) + .headers(headers != null ? headers : NULL_HEADERS) + .build(); + + OkHttpClient progressClient = mClient.clone(); + + progressClient.networkInterceptors().add(chain -> { + Response originalResponse = chain.proceed(chain.request()); + return originalResponse.newBuilder() + .body(new ProgressResponseBody(originalResponse.body(), page)) + .build(); + }); + subscriber.onNext(progressClient.newCall(request).execute()); + } + subscriber.onCompleted(); + } catch (Throwable e) { + subscriber.onError(e); + } + }).retry(3); + } + public CookieStore getCookies() { return cookieManager.getCookieStore(); } + private static class ProgressResponseBody extends ResponseBody { + + private final ResponseBody responseBody; + private final ProgressListener progressListener; + private BufferedSource bufferedSource; + + public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override public MediaType contentType() { + return responseBody.contentType(); + } + + @Override public long contentLength() throws IOException { + return responseBody.contentLength(); + } + + @Override public BufferedSource source() throws IOException { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + long totalBytesRead = 0L; + @Override public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += bytesRead != -1 ? bytesRead : 0; + progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1); + return bytesRead; + } + }; + } + } + + public interface ProgressListener { + void update(long bytesRead, long contentLength, boolean done); + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java b/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java index 076bc33ca0..15fe0cbcc9 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/models/Page.java @@ -1,12 +1,15 @@ package eu.kanade.mangafeed.data.models; -public class Page { +import eu.kanade.mangafeed.data.helpers.NetworkHelper; + +public class Page implements NetworkHelper.ProgressListener { private int pageNumber; private String url; private String imageUrl; private String imagePath; private int status; + private int progress; public static final int DOWNLOAD = 0; public static final int READY = 1; @@ -55,6 +58,10 @@ public class Page { this.status = status; } + public int getProgress() { + return progress; + } + @Override public String toString() { return "Page{" + @@ -64,4 +71,9 @@ public class Page { '}'; } + @Override + public void update(long bytesRead, long contentLength, boolean done) { + progress = (int) ((100 * bytesRead) / contentLength); + } + } 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 8192622507..b56b9f5db6 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 @@ -2,9 +2,6 @@ package eu.kanade.mangafeed.injection.module; import android.app.Application; -import com.bumptech.glide.Glide; -import com.bumptech.glide.RequestManager; - import javax.inject.Singleton; import dagger.Module; @@ -59,9 +56,4 @@ public class DataModule { return new SourceManager(app); } - @Provides - @Singleton - RequestManager provideGlideDownloader(Application app) { - return Glide.with(app); - } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/ReaderPresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/ReaderPresenter.java index 46248f8251..16522ddd37 100644 --- a/app/src/main/java/eu/kanade/mangafeed/presenter/ReaderPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/ReaderPresenter.java @@ -2,11 +2,6 @@ package eu.kanade.mangafeed.presenter; import android.os.Bundle; -import com.bumptech.glide.RequestManager; -import com.bumptech.glide.request.FutureTarget; -import com.bumptech.glide.request.target.Target; - -import java.io.File; import java.util.List; import javax.inject.Inject; @@ -28,7 +23,6 @@ import timber.log.Timber; public class ReaderPresenter extends BasePresenter { @Inject PreferencesHelper prefs; - @Inject RequestManager glideDownloader; private Source source; private Chapter chapter; @@ -106,28 +100,11 @@ public class ReaderPresenter extends BasePresenter { source.getRemainingImageUrlsFromPageList(pageList) .doOnNext(this::replacePageUrl) ) - .flatMap(this::downloadImage) + .flatMap(source::getCachedImage) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } - private Observable downloadImage(Page page) { - if (page.getImageUrl() != null) { - FutureTarget future = glideDownloader.load(page.getImageUrl()) - .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); - - try { - File cacheFile = future.get(); - page.setImagePath(cacheFile.getCanonicalPath()); - page.setStatus(Page.READY); - } catch (Exception e) { - page.setStatus(Page.ERROR); - } - } - - return Observable.just(page); - } - private void replacePageUrl(Page page) { for (int i = 0; i < pageList.size(); i++) { if (pageList.get(i).getPageNumber() == page.getPageNumber()) { diff --git a/app/src/main/java/eu/kanade/mangafeed/sources/base/Source.java b/app/src/main/java/eu/kanade/mangafeed/sources/base/Source.java index edcbf9ca5f..a403d936f1 100644 --- a/app/src/main/java/eu/kanade/mangafeed/sources/base/Source.java +++ b/app/src/main/java/eu/kanade/mangafeed/sources/base/Source.java @@ -102,6 +102,31 @@ public abstract class Source extends BaseSource { .subscribeOn(Schedulers.io()); } + public Observable getCachedImage(final Page page) { + Observable obs = Observable.just(page); + if (page.getImageUrl() == null) + return obs; + + if (!mCacheManager.isImageInCache(page.getImageUrl())) { + obs = mNetworkService.getProgressResponse(page.getImageUrl(), mRequestHeaders, page) + .flatMap(resp -> { + if (!mCacheManager.putImageToDiskCache(page.getImageUrl(), resp)) { + throw new IllegalStateException("Unable to save image"); + } + return Observable.just(page); + }); + } + + return obs.flatMap(p -> { + page.setImagePath(mCacheManager.getImagePath(page.getImageUrl())); + page.setStatus(Page.READY); + return Observable.just(page); + }).onErrorResumeNext(e -> { + page.setStatus(Page.ERROR); + return Observable.just(page); + }); + } + public void savePageList(String chapterUrl, List pages) { if (pages != null) mCacheManager.putPageUrlsToDiskCache(chapterUrl, pages); diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ReaderPageFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ReaderPageFragment.java index 9354175b42..90bb99e339 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ReaderPageFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/ReaderPageFragment.java @@ -6,25 +6,35 @@ import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; +import java.util.concurrent.TimeUnit; + import butterknife.Bind; import butterknife.ButterKnife; import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.models.Page; import eu.kanade.mangafeed.ui.activity.ReaderActivity; +import rx.Observable; +import rx.Subscription; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; public class ReaderPageFragment extends Fragment { @Bind(R.id.page_image_view) SubsamplingScaleImageView imageView; + @Bind(R.id.progress_container) LinearLayout progressContainer; @Bind(R.id.progress) ProgressBar progressBar; + @Bind(R.id.progress_text) TextView progressText; @Bind(R.id.image_error) TextView errorText; private Page page; + private Subscription progressSubscription; public static ReaderPageFragment newInstance(Page page) { ReaderPageFragment fragment = new ReaderPageFragment(); @@ -35,11 +45,11 @@ public class ReaderPageFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setRetainInstance(true); } public void replacePage(Page page) { + unsubscribeProgress(); this.page = page; loadImage(); } @@ -55,13 +65,13 @@ public class ReaderPageFragment extends Fragment { switch (page.getStatus()) { case (Page.READY): imageView.setImage(ImageSource.uri(page.getImagePath()).tilingDisabled()); - progressBar.setVisibility(View.GONE); + progressContainer.setVisibility(View.GONE); break; case (Page.DOWNLOAD): - progressBar.setVisibility(View.VISIBLE); + progressContainer.setVisibility(View.VISIBLE); break; case (Page.ERROR): - progressBar.setVisibility(View.GONE); + progressContainer.setVisibility(View.GONE); errorText.setVisibility(View.VISIBLE); } @@ -78,9 +88,42 @@ public class ReaderPageFragment extends Fragment { imageView.setOnTouchListener((v, motionEvent) -> ((ReaderActivity) getActivity()).onImageTouch(motionEvent)); + observeProgress(); loadImage(); return view; } + @Override + public void onStop() { + super.onStop(); + unsubscribeProgress(); + } + + private void observeProgress() { + if (page == null || page.getStatus() != Page.DOWNLOAD) + return; + + progressSubscription = Observable.interval(75, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(tick -> { + if (page.getProgress() == 0) { + progressText.setText(R.string.downloading); + } + else if (page.getProgress() == 100) { + progressContainer.setVisibility(View.GONE); + unsubscribeProgress(); + } + else { + progressText.setText(getString(R.string.download_progress, page.getProgress())); + } + }); + } + + private void unsubscribeProgress() { + if (progressSubscription != null) + progressSubscription.unsubscribe(); + } + } diff --git a/app/src/main/res/layout/fragment_page.xml b/app/src/main/res/layout/fragment_page.xml index 14945aebdc..bed5761b90 100644 --- a/app/src/main/res/layout/fragment_page.xml +++ b/app/src/main/res/layout/fragment_page.xml @@ -5,13 +5,29 @@ android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"> - + android:id="@+id/progress_container" + android:orientation="vertical"> + + + + + + Loading… Added to favorites Favorite + Downloading… + Downloaded %1$d%%