diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 73870258b0..4c2d5e7209 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -46,6 +46,9 @@
+
+
diff --git a/app/src/main/java/eu/kanade/mangafeed/data/helpers/DownloadManager.java b/app/src/main/java/eu/kanade/mangafeed/data/helpers/DownloadManager.java
new file mode 100644
index 0000000000..fe3badc8d8
--- /dev/null
+++ b/app/src/main/java/eu/kanade/mangafeed/data/helpers/DownloadManager.java
@@ -0,0 +1,128 @@
+package eu.kanade.mangafeed.data.helpers;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import eu.kanade.mangafeed.data.models.Chapter;
+import eu.kanade.mangafeed.data.models.Manga;
+import eu.kanade.mangafeed.data.models.Page;
+import eu.kanade.mangafeed.events.DownloadChapterEvent;
+import eu.kanade.mangafeed.sources.base.Source;
+import eu.kanade.mangafeed.util.DiskUtils;
+import rx.Observable;
+import rx.Subscription;
+import rx.schedulers.Schedulers;
+import rx.subjects.PublishSubject;
+
+public class DownloadManager {
+
+ private PublishSubject downloadsSubject;
+ private Subscription downloadsSubscription;
+
+ private Context context;
+ private SourceManager sourceManager;
+
+ public DownloadManager(Context context, SourceManager sourceManager) {
+ this.context = context;
+ this.sourceManager = sourceManager;
+
+ initializeDownloadSubscription();
+ }
+
+ private void initializeDownloadSubscription() {
+ if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed()) {
+ downloadsSubscription.unsubscribe();
+ }
+
+ downloadsSubject = PublishSubject.create();
+
+ downloadsSubscription = downloadsSubject
+ .subscribeOn(Schedulers.io())
+ .concatMap(event -> downloadChapter(event.getManga(), event.getChapter()))
+ .onBackpressureBuffer()
+ .subscribe();
+ }
+
+ private Observable downloadChapter(Manga manga, Chapter chapter) {
+ final Source source = sourceManager.get(manga.source);
+ final File chapterDirectory = new File(getDownloadsDirectory(), getChapterDirectory(chapter));
+
+ return source
+ .pullPageListFromNetwork(chapter.url)
+ // Ensure we don't download a chapter already downloaded
+ .filter(pages -> !isChapterDownloaded(chapterDirectory, pages))
+ // Get all the URLs to the source images, fetch pages if necessary
+ .flatMap(pageList -> Observable.merge(
+ Observable.from(pageList).filter(page -> page.getImageUrl() != null),
+ source.getRemainingImageUrlsFromPageList(pageList)))
+ // Start downloading images
+ .flatMap(page -> getDownloadedImage(page, source, chapterDirectory));
+ }
+
+ private File getDownloadsDirectory() {
+ // TODO
+ return new File(DiskUtils.getStorageDirectories(context)[0]);
+ }
+
+ private String getChapterDirectory(Chapter chapter) {
+ return chapter.name.replaceAll("[^a-zA-Z0-9.-]", "_");
+ }
+
+ private String getImageFilename(Page page) {
+ return page.getImageUrl().substring(
+ page.getImageUrl().lastIndexOf("/") + 1,
+ page.getImageUrl().length());
+ }
+
+ private boolean isChapterDownloaded(File chapterDir, List pages) {
+ return chapterDir.exists() && chapterDir.listFiles().length == pages.size();
+ }
+
+ private boolean isImageDownloaded(File imagePath) {
+ return imagePath.exists() && !imagePath.isDirectory();
+ }
+
+ public Observable getDownloadedImage(final Page page, Source source, File chapterDir) {
+ Observable obs = Observable.just(page);
+ if (page.getImageUrl() == null)
+ return obs;
+
+ String imageFilename = getImageFilename(page);
+ File imagePath = new File(chapterDir, imageFilename);
+
+ if (!isImageDownloaded(imagePath)) {
+ page.setStatus(Page.DOWNLOAD_IMAGE);
+ obs = downloadImage(page, source, chapterDir, imageFilename);
+ }
+
+ return obs.flatMap(p -> {
+ page.setImagePath(imagePath.getAbsolutePath());
+ page.setStatus(Page.READY);
+ return Observable.just(page);
+ }).onErrorResumeNext(e -> {
+ page.setStatus(Page.ERROR);
+ return Observable.just(page);
+ });
+ }
+
+ private Observable downloadImage(final Page page, Source source, File chapterDir, String imageFilename) {
+ return source.getImageProgressResponse(page)
+ .flatMap(resp -> {
+ try {
+ DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), chapterDir, imageFilename);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new IllegalStateException("Unable to save image");
+ }
+ return Observable.just(page);
+ });
+ }
+
+ public PublishSubject getDownloadsSubject() {
+ return downloadsSubject;
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/mangafeed/data/helpers/PreferencesHelper.java b/app/src/main/java/eu/kanade/mangafeed/data/helpers/PreferencesHelper.java
index cb73a99bff..51b400eec6 100644
--- a/app/src/main/java/eu/kanade/mangafeed/data/helpers/PreferencesHelper.java
+++ b/app/src/main/java/eu/kanade/mangafeed/data/helpers/PreferencesHelper.java
@@ -6,6 +6,7 @@ import android.preference.PreferenceManager;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.sources.base.Source;
+import eu.kanade.mangafeed.util.DiskUtils;
public class PreferencesHelper {
@@ -53,4 +54,9 @@ public class PreferencesHelper {
.apply();
}
+ public String getDownloadsDirectory() {
+ return mPref.getString(getKey(R.string.pref_download_directory_key),
+ DiskUtils.getStorageDirectories(context)[0]);
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/mangafeed/data/services/DownloadService.java b/app/src/main/java/eu/kanade/mangafeed/data/services/DownloadService.java
new file mode 100644
index 0000000000..f97873f17e
--- /dev/null
+++ b/app/src/main/java/eu/kanade/mangafeed/data/services/DownloadService.java
@@ -0,0 +1,58 @@
+package eu.kanade.mangafeed.data.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import javax.inject.Inject;
+
+import de.greenrobot.event.EventBus;
+import eu.kanade.mangafeed.App;
+import eu.kanade.mangafeed.data.helpers.DownloadManager;
+import eu.kanade.mangafeed.events.DownloadChapterEvent;
+import eu.kanade.mangafeed.util.AndroidComponentUtil;
+import eu.kanade.mangafeed.util.EventBusHook;
+
+public class DownloadService extends Service {
+
+ @Inject DownloadManager downloadManager;
+
+ public static Intent getStartIntent(Context context) {
+ return new Intent(context, DownloadService.class);
+ }
+
+ public static boolean isRunning(Context context) {
+ return AndroidComponentUtil.isServiceRunning(context, DownloadService.class);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ App.get(this).getComponent().inject(this);
+
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @EventBusHook
+ public void onEvent(DownloadChapterEvent event) {
+ downloadManager.getDownloadsSubject().onNext(event);
+ }
+
+ @Override
+ public void onDestroy() {
+ EventBus.getDefault().unregister(this);
+ super.onDestroy();
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/mangafeed/events/DownloadChapterEvent.java b/app/src/main/java/eu/kanade/mangafeed/events/DownloadChapterEvent.java
new file mode 100644
index 0000000000..4cc9352657
--- /dev/null
+++ b/app/src/main/java/eu/kanade/mangafeed/events/DownloadChapterEvent.java
@@ -0,0 +1,22 @@
+package eu.kanade.mangafeed.events;
+
+import eu.kanade.mangafeed.data.models.Chapter;
+import eu.kanade.mangafeed.data.models.Manga;
+
+public class DownloadChapterEvent {
+ private Manga manga;
+ private Chapter chapter;
+
+ public DownloadChapterEvent(Manga manga, Chapter chapter) {
+ this.manga = manga;
+ this.chapter = chapter;
+ }
+
+ public Manga getManga() {
+ return manga;
+ }
+
+ public Chapter getChapter() {
+ return chapter;
+ }
+}
diff --git a/app/src/main/java/eu/kanade/mangafeed/injection/component/AppComponent.java b/app/src/main/java/eu/kanade/mangafeed/injection/component/AppComponent.java
index 90a117d18c..4cc52fc5d1 100644
--- a/app/src/main/java/eu/kanade/mangafeed/injection/component/AppComponent.java
+++ b/app/src/main/java/eu/kanade/mangafeed/injection/component/AppComponent.java
@@ -5,6 +5,7 @@ import android.app.Application;
import javax.inject.Singleton;
import dagger.Component;
+import eu.kanade.mangafeed.data.services.DownloadService;
import eu.kanade.mangafeed.data.services.LibraryUpdateService;
import eu.kanade.mangafeed.injection.module.AppModule;
import eu.kanade.mangafeed.injection.module.DataModule;
@@ -42,6 +43,7 @@ public interface AppComponent {
void inject(Source source);
void inject(LibraryUpdateService libraryUpdateService);
+ void inject(DownloadService downloadService);
Application application();
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 b56b9f5db6..c98dfcbce7 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
@@ -8,11 +8,10 @@ import dagger.Module;
import dagger.Provides;
import eu.kanade.mangafeed.data.caches.CacheManager;
import eu.kanade.mangafeed.data.helpers.DatabaseHelper;
+import eu.kanade.mangafeed.data.helpers.DownloadManager;
import eu.kanade.mangafeed.data.helpers.NetworkHelper;
import eu.kanade.mangafeed.data.helpers.PreferencesHelper;
import eu.kanade.mangafeed.data.helpers.SourceManager;
-import rx.Scheduler;
-import rx.schedulers.Schedulers;
/**
* Provide dependencies to the DataManager, mainly Helper classes and Retrofit services.
@@ -32,12 +31,6 @@ public class DataModule {
return new DatabaseHelper(app);
}
- @Provides
- @Singleton
- Scheduler provideSubscribeScheduler() {
- return Schedulers.io();
- }
-
@Provides
@Singleton
CacheManager provideCacheManager(Application app) {
@@ -56,4 +49,10 @@ public class DataModule {
return new SourceManager(app);
}
+ @Provides
+ @Singleton
+ DownloadManager provideDownloadManager(Application app, SourceManager sourceManager) {
+ return new DownloadManager(app, sourceManager);
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java
index ad83d3d0b6..6b85278553 100644
--- a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java
+++ b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java
@@ -85,6 +85,10 @@ public class MangaChaptersPresenter extends BasePresenter
}
}
+ public Manga getManga() {
+ return manga;
+ }
+
public void refreshChapters() {
if (getView() != null)
getView().setSwipeRefreshing();
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 afd068b8c7..9005084f2d 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
@@ -4,6 +4,7 @@ package eu.kanade.mangafeed.sources.base;
import android.content.Context;
import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Response;
import java.util.ArrayList;
import java.util.List;
@@ -123,7 +124,7 @@ public abstract class Source extends BaseSource {
}
private Observable cacheImage(final Page page) {
- return mNetworkService.getProgressResponse(page.getImageUrl(), mRequestHeaders, page)
+ return getImageProgressResponse(page)
.flatMap(resp -> {
if (!mCacheManager.putImageToDiskCache(page.getImageUrl(), resp)) {
throw new IllegalStateException("Unable to save image");
@@ -132,6 +133,10 @@ public abstract class Source extends BaseSource {
});
}
+ public Observable getImageProgressResponse(final Page page) {
+ return mNetworkService.getProgressResponse(page.getImageUrl(), mRequestHeaders, 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/MangaChaptersFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java
index f2d94f7b91..0fb6d30e42 100644
--- a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java
+++ b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/MangaChaptersFragment.java
@@ -18,8 +18,11 @@ import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
+import de.greenrobot.event.EventBus;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.models.Chapter;
+import eu.kanade.mangafeed.data.services.DownloadService;
+import eu.kanade.mangafeed.events.DownloadChapterEvent;
import eu.kanade.mangafeed.presenter.MangaChaptersPresenter;
import eu.kanade.mangafeed.ui.activity.MangaDetailActivity;
import eu.kanade.mangafeed.ui.activity.ReaderActivity;
@@ -28,6 +31,8 @@ import eu.kanade.mangafeed.ui.adapter.ChaptersAdapter;
import eu.kanade.mangafeed.ui.fragment.base.BaseRxFragment;
import nucleus.factory.RequiresPresenter;
import rx.Observable;
+import rx.Subscription;
+import rx.schedulers.Schedulers;
@RequiresPresenter(MangaChaptersPresenter.class)
public class MangaChaptersFragment extends BaseRxFragment implements
@@ -39,6 +44,7 @@ public class MangaChaptersFragment extends BaseRxFragment {
+ EventBus.getDefault().post(
+ new DownloadChapterEvent(getPresenter().getManga(), chapter));
+ downloadSubscription.unsubscribe();
+ });
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/mangafeed/util/DiskUtils.java b/app/src/main/java/eu/kanade/mangafeed/util/DiskUtils.java
index c91593e7f3..a30ccfb3c4 100644
--- a/app/src/main/java/eu/kanade/mangafeed/util/DiskUtils.java
+++ b/app/src/main/java/eu/kanade/mangafeed/util/DiskUtils.java
@@ -18,7 +18,7 @@ import okio.BufferedSource;
import okio.Okio;
public final class DiskUtils {
- private static final Pattern DIR_SEPORATOR = Pattern.compile("/");
+ private static final Pattern DIR_SEPARATOR = Pattern.compile("/");
private DiskUtils() {
throw new AssertionError();
@@ -58,7 +58,7 @@ public final class DiskUtils {
rawUserId = "";
} else {
final String path = Environment.getExternalStorageDirectory().getAbsolutePath();
- final String[] folders = DIR_SEPORATOR.split(path);
+ final String[] folders = DIR_SEPARATOR.split(path);
final String lastFolder = folders[folders.length - 1];
boolean isDigit = false;
@@ -114,18 +114,17 @@ public final class DiskUtils {
return sb.toString();
}
- public static File saveBufferedSourceToDirectory(BufferedSource bufferedSource, String directory, String name) throws IOException {
- File fileDirectory = new File(directory);
- if (!fileDirectory.exists()) {
- if (!fileDirectory.mkdirs()) {
+ public static File saveBufferedSourceToDirectory(BufferedSource bufferedSource, File directory, String name) throws IOException {
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
throw new IOException("Failed Creating Directory");
}
}
- File writeFile = new File(fileDirectory, name);
+ File writeFile = new File(directory, name);
if (writeFile.exists()) {
if (writeFile.delete()) {
- writeFile = new File(fileDirectory, name);
+ writeFile = new File(directory, name);
} else {
throw new IOException("Failed Deleting Existing File for Overwrite");
}
diff --git a/app/src/main/res/drawable-hdpi/ic_file_download.png b/app/src/main/res/drawable-hdpi/ic_file_download.png
new file mode 100644
index 0000000000..a1714bc050
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-ldpi/ic_file_download.png b/app/src/main/res/drawable-ldpi/ic_file_download.png
new file mode 100644
index 0000000000..be2b7d3b9a
Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_file_download.png b/app/src/main/res/drawable-mdpi/ic_file_download.png
new file mode 100644
index 0000000000..5cf7333355
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-tvdpi/ic_file_download.png b/app/src/main/res/drawable-tvdpi/ic_file_download.png
new file mode 100644
index 0000000000..dd0db5bd97
Binary files /dev/null and b/app/src/main/res/drawable-tvdpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_file_download.png b/app/src/main/res/drawable-xhdpi/ic_file_download.png
new file mode 100644
index 0000000000..c9843ba8a2
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_file_download.png b/app/src/main/res/drawable-xxhdpi/ic_file_download.png
new file mode 100644
index 0000000000..ea828d089a
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_file_download.png b/app/src/main/res/drawable-xxxhdpi/ic_file_download.png
new file mode 100644
index 0000000000..0f286755cd
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_file_download.png differ
diff --git a/app/src/main/res/menu/chapter_selection.xml b/app/src/main/res/menu/chapter_selection.xml
index 8c435ef842..ce8a808cd5 100644
--- a/app/src/main/res/menu/chapter_selection.xml
+++ b/app/src/main/res/menu/chapter_selection.xml
@@ -3,6 +3,11 @@