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 index 9c6a94169..9b6c62ad1 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/helpers/DownloadManager.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/helpers/DownloadManager.java @@ -12,11 +12,11 @@ import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Type; -import java.util.ArrayList; import java.util.List; import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Download; +import eu.kanade.mangafeed.data.models.DownloadQueue; import eu.kanade.mangafeed.data.models.Manga; import eu.kanade.mangafeed.data.models.Page; import eu.kanade.mangafeed.events.DownloadChapterEvent; @@ -37,7 +37,9 @@ public class DownloadManager { private PreferencesHelper preferences; private Gson gson; - private List queue; + private DownloadQueue queue; + + public static final String PAGE_LIST_FILE = "index.json"; public DownloadManager(Context context, SourceManager sourceManager, PreferencesHelper preferences) { this.context = context; @@ -45,7 +47,7 @@ public class DownloadManager { this.preferences = preferences; this.gson = new Gson(); - queue = new ArrayList<>(); + queue = new DownloadQueue(); initializeDownloadSubscription(); } @@ -78,7 +80,7 @@ public class DownloadManager { final Source source = sourceManager.get(event.getManga().source); // If the chapter is already queued, don't add it again - for (Download download : queue) { + for (Download download : queue.get()) { if (download.chapter.id == event.getChapter().id) return true; } @@ -119,7 +121,10 @@ public class DownloadManager { .pullPageListFromNetwork(download.chapter.url) .subscribeOn(Schedulers.io()) // Add resulting pages to download object - .doOnNext(pages -> download.pages = pages) + .doOnNext(pages -> { + download.pages = pages; + download.setStatus(Download.DOWNLOADING); + }) // Get all the URLs to the source images, fetch pages if necessary .flatMap(pageList -> Observable.merge( Observable.from(pageList).filter(page -> page.getImageUrl() != null), @@ -127,7 +132,7 @@ public class DownloadManager { // Start downloading images, consider we can have downloaded images already .concatMap(page -> getDownloadedImage(page, download.source, download.directory)) // Remove from the queue - .doOnCompleted(() -> removeFromQueue(download)); + .doOnCompleted(() -> onChapterDownloaded(download)); } // Get downloaded image if exists, otherwise download it with the method below @@ -179,15 +184,15 @@ public class DownloadManager { return imagePath.exists() && !imagePath.isDirectory(); } - private void removeFromQueue(final Download download) { + private void onChapterDownloaded(final Download download) { + download.setStatus(Download.DOWNLOADED); savePageList(download.source, download.manga, download.chapter, download.pages); - queue.remove(download); } // Return the page list from the chapter's directory if it exists, null otherwise public List getSavedPageList(Source source, Manga manga, Chapter chapter) { File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter); - File pagesFile = new File(chapterDir, "index.json"); + File pagesFile = new File(chapterDir, PAGE_LIST_FILE); try { JsonReader reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath())); @@ -202,7 +207,7 @@ public class DownloadManager { // Save the page list to the chapter's directory public void savePageList(Source source, Manga manga, Chapter chapter, List pages) { File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter); - File pagesFile = new File(chapterDir, "index.json"); + File pagesFile = new File(chapterDir, PAGE_LIST_FILE); FileOutputStream out; try { @@ -230,4 +235,8 @@ public class DownloadManager { File path = getAbsoluteChapterDirectory(source, manga, chapter); DiskUtils.deleteFiles(path); } + + public DownloadQueue getQueue() { + return queue; + } } diff --git a/app/src/main/java/eu/kanade/mangafeed/data/models/Download.java b/app/src/main/java/eu/kanade/mangafeed/data/models/Download.java index f55d184dd..08e4f98a0 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/models/Download.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/models/Download.java @@ -4,6 +4,7 @@ import java.io.File; import java.util.List; import eu.kanade.mangafeed.sources.base.Source; +import rx.subjects.PublishSubject; public class Download { public Source source; @@ -12,9 +13,38 @@ public class Download { public List pages; public File directory; + public transient volatile int totalProgress; + private transient volatile int status; + + private transient PublishSubject statusSubject; + + public static final int QUEUE = 0; + public static final int DOWNLOADING = 1; + public static final int DOWNLOADED = 2; + public static final int ERROR = 3; + + public Download(Source source, Manga manga, Chapter chapter) { this.source = source; this.manga = manga; this.chapter = chapter; } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + notifyStatus(); + } + + public void setStatusSubject(PublishSubject subject) { + this.statusSubject = subject; + } + + private void notifyStatus() { + if (statusSubject != null) + statusSubject.onNext(this); + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/mangafeed/data/models/DownloadQueue.java b/app/src/main/java/eu/kanade/mangafeed/data/models/DownloadQueue.java new file mode 100644 index 000000000..21aec349e --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/data/models/DownloadQueue.java @@ -0,0 +1,43 @@ +package eu.kanade.mangafeed.data.models; + +import java.util.ArrayList; +import java.util.List; + +import rx.Observable; +import rx.subjects.PublishSubject; + +public class DownloadQueue { + + private List queue; + private PublishSubject statusSubject; + + public DownloadQueue() { + queue = new ArrayList<>(); + statusSubject = PublishSubject.create(); + } + + public void add(Download download) { + download.setStatusSubject(statusSubject); + queue.add(download); + } + + public void remove(Download download) { + queue.remove(download); + download.setStatusSubject(null); + } + + public List get() { + return queue; + } + + public Observable getActiveDownloads() { + return Observable.from(queue) + .filter(download -> download.getStatus() == Download.DOWNLOADING); + } + + public Observable getStatusObservable() { + return statusSubject + .startWith(getActiveDownloads()); + } + +} 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 8006b4094..1856ac922 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 @@ -9,8 +9,8 @@ public class Page implements NetworkHelper.ProgressListener { private String url; private String imageUrl; private String imagePath; - private transient int status; - private transient int progress; + private transient volatile int status; + private transient volatile int progress; private transient BehaviorSubject statusSubject; 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 c24ba8848..6e1df9d13 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 @@ -10,6 +10,7 @@ import eu.kanade.mangafeed.data.services.LibraryUpdateService; import eu.kanade.mangafeed.injection.module.AppModule; import eu.kanade.mangafeed.injection.module.DataModule; import eu.kanade.mangafeed.presenter.CataloguePresenter; +import eu.kanade.mangafeed.presenter.DownloadQueuePresenter; import eu.kanade.mangafeed.presenter.LibraryPresenter; import eu.kanade.mangafeed.presenter.MangaChaptersPresenter; import eu.kanade.mangafeed.presenter.MangaDetailPresenter; @@ -37,6 +38,7 @@ public interface AppComponent { void inject(MangaInfoPresenter mangaInfoPresenter); void inject(MangaChaptersPresenter mangaChaptersPresenter); void inject(ReaderPresenter readerPresenter); + void inject(DownloadQueuePresenter downloadQueuePresenter); void inject(ReaderActivity readerActivity); void inject(SettingsAccountsFragment settingsAccountsFragment); diff --git a/app/src/main/java/eu/kanade/mangafeed/presenter/DownloadQueuePresenter.java b/app/src/main/java/eu/kanade/mangafeed/presenter/DownloadQueuePresenter.java new file mode 100644 index 000000000..34fd9a016 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/DownloadQueuePresenter.java @@ -0,0 +1,107 @@ +package eu.kanade.mangafeed.presenter; + +import android.os.Bundle; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import eu.kanade.mangafeed.data.helpers.DownloadManager; +import eu.kanade.mangafeed.data.models.Download; +import eu.kanade.mangafeed.data.models.DownloadQueue; +import eu.kanade.mangafeed.data.models.Page; +import eu.kanade.mangafeed.ui.fragment.DownloadQueueFragment; +import rx.Observable; +import rx.Subscription; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; +import timber.log.Timber; + +public class DownloadQueuePresenter extends BasePresenter { + + @Inject DownloadManager downloadManager; + + private DownloadQueue downloadQueue; + private Subscription statusSubscription; + private HashMap progressSubscriptions; + + public final static int GET_DOWNLOAD_QUEUE = 1; + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + downloadQueue = downloadManager.getQueue(); + progressSubscriptions = new HashMap<>(); + + restartableLatestCache(GET_DOWNLOAD_QUEUE, + () -> Observable.just(downloadQueue.get()), + DownloadQueueFragment::onNextDownloads, + (view, error) -> Timber.e(error.getMessage())); + + if (savedState == null) + start(GET_DOWNLOAD_QUEUE); + } + + @Override + protected void onTakeView(DownloadQueueFragment view) { + super.onTakeView(view); + + statusSubscription = downloadQueue.getStatusObservable() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(download -> { + processStatus(download, view); + }); + } + + @Override + protected void onDropView() { + destroySubscriptions(); + super.onDropView(); + } + + private void processStatus(Download download, DownloadQueueFragment view) { + switch (download.getStatus()) { + case Download.DOWNLOADING: + observeProgress(download, view); + break; + case Download.DOWNLOADED: + unsubscribeProgress(download); + download.totalProgress = download.pages.size() * 100; + view.updateProgress(download); + break; + } + } + + private void observeProgress(Download download, DownloadQueueFragment view) { + Subscription subscription = Observable.interval(50, TimeUnit.MILLISECONDS, Schedulers.newThread()) + .flatMap(tick -> Observable.from(download.pages) + .map(Page::getProgress) + .reduce((x, y) -> x + y)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(progress -> { + download.totalProgress = progress; + view.updateProgress(download); + }); + + progressSubscriptions.put(download, subscription); + } + + private void unsubscribeProgress(Download download) { + Subscription subscription = progressSubscriptions.remove(download); + if (subscription != null) + subscription.unsubscribe(); + } + + private void destroySubscriptions() { + for (Subscription subscription : progressSubscriptions.values()) { + subscription.unsubscribe(); + } + progressSubscriptions.clear(); + + remove(statusSubscription); + } + +} 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 81bbaf493..4013d0f1b 100644 --- a/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java @@ -128,7 +128,7 @@ public class MangaChaptersPresenter extends BasePresenter .toList() .flatMap(db::insertChapters) .observeOn(AndroidSchedulers.mainThread()) - .doOnCompleted( () -> remove(markReadSubscription) ) + .doOnCompleted(() -> remove(markReadSubscription)) .subscribe(result -> { })); } @@ -137,21 +137,10 @@ public class MangaChaptersPresenter extends BasePresenter add(downloadSubscription = selectedChapters .doOnCompleted(() -> remove(downloadSubscription)) .subscribe(chapter -> { - EventBus.getDefault().post( - new DownloadChapterEvent(manga, chapter)); + EventBus.getDefault().post(new DownloadChapterEvent(manga, chapter)); })); } - public void checkIsChapterDownloaded(Chapter chapter) { - File dir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); - - if (dir.exists() && dir.listFiles().length > 0) { - chapter.downloaded = Chapter.DOWNLOADED; - } else { - chapter.downloaded = Chapter.NOT_DOWNLOADED; - } - } - public void deleteChapters(Observable selectedChapters) { deleteSubscription = selectedChapters .doOnCompleted( () -> remove(deleteSubscription) ) @@ -160,4 +149,16 @@ public class MangaChaptersPresenter extends BasePresenter chapter.downloaded = Chapter.NOT_DOWNLOADED; }); } + + public void checkIsChapterDownloaded(Chapter chapter) { + File dir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); + File pageList = new File(dir, DownloadManager.PAGE_LIST_FILE); + + if (dir.exists() && dir.listFiles().length > 0 && pageList.exists()) { + chapter.downloaded = Chapter.DOWNLOADED; + } else { + chapter.downloaded = Chapter.NOT_DOWNLOADED; + } + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java b/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java index 34b60002b..a1035f778 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/activity/MainActivity.java @@ -15,6 +15,7 @@ import butterknife.Bind; import butterknife.ButterKnife; import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.ui.activity.base.BaseActivity; +import eu.kanade.mangafeed.ui.fragment.DownloadQueueFragment; import eu.kanade.mangafeed.ui.fragment.LibraryFragment; import eu.kanade.mangafeed.ui.fragment.SourceFragment; @@ -51,6 +52,9 @@ public class MainActivity extends BaseActivity { new PrimaryDrawerItem() .withName(R.string.catalogues_title) .withIdentifier(R.id.nav_drawer_catalogues), + new PrimaryDrawerItem() + .withName(R.string.download_title) + .withIdentifier(R.id.nav_drawer_downloads), new PrimaryDrawerItem() .withName(R.string.settings_title) .withIdentifier(R.id.nav_drawer_settings) @@ -70,6 +74,9 @@ public class MainActivity extends BaseActivity { case R.id.nav_drawer_catalogues: setFragment(SourceFragment.newInstance()); break; + case R.id.nav_drawer_downloads: + setFragment(DownloadQueueFragment.newInstance()); + break; case R.id.nav_drawer_settings: startActivity(new Intent(this, SettingsActivity.class)); break; diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/fragment/DownloadQueueFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/DownloadQueueFragment.java new file mode 100644 index 000000000..bca0e22ac --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/fragment/DownloadQueueFragment.java @@ -0,0 +1,66 @@ +package eu.kanade.mangafeed.ui.fragment; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import butterknife.Bind; +import butterknife.ButterKnife; +import eu.kanade.mangafeed.R; +import eu.kanade.mangafeed.data.models.Download; +import eu.kanade.mangafeed.presenter.DownloadQueuePresenter; +import eu.kanade.mangafeed.ui.fragment.base.BaseRxFragment; +import eu.kanade.mangafeed.ui.holder.DownloadHolder; +import nucleus.factory.RequiresPresenter; +import uk.co.ribot.easyadapter.EasyRecyclerAdapter; + +@RequiresPresenter(DownloadQueuePresenter.class) +public class DownloadQueueFragment extends BaseRxFragment { + + @Bind(R.id.download_list) RecyclerView downloadList; + private EasyRecyclerAdapter adapter; + + public static DownloadQueueFragment newInstance() { + return new DownloadQueueFragment(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_download_queue, container, false); + ButterKnife.bind(this, view); + + setToolbarTitle(R.string.download_title); + + downloadList.setLayoutManager(new LinearLayoutManager(getActivity())); + createAdapter(); + + return view; + } + + private void createAdapter() { + adapter = new EasyRecyclerAdapter<>(getActivity(), DownloadHolder.class); + downloadList.setAdapter(adapter); + } + + public void onNextDownloads(List downloads) { + adapter.setItems(downloads); + } + + // TODO use a better approach + public void updateProgress(Download download) { + for (int i = 0; i < adapter.getItems().size(); i++) { + if (adapter.getItem(i) == download) { + adapter.notifyItemChanged(i); + break; + } + } + } + +} 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 ee624999a..3fe706f09 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 @@ -2,7 +2,6 @@ package eu.kanade.mangafeed.ui.fragment; import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.view.ActionMode; import android.support.v7.widget.LinearLayoutManager; @@ -41,7 +40,7 @@ public class MangaChaptersFragment extends BaseRxFragment { + + @ViewId(R.id.download_title) TextView downloadTitle; + @ViewId(R.id.download_progress) ProgressBar downloadProgress; + + public DownloadHolder(View view) { + super(view); + } + + @Override + public void onSetValues(Download download, PositionInfo positionInfo) { + downloadTitle.setText(download.chapter.name); + + if (download.pages == null) { + downloadProgress.setProgress(0); + } else { + downloadProgress.setMax(download.pages.size() * 100); + downloadProgress.setProgress(download.totalProgress); + } + } + +} diff --git a/app/src/main/res/layout/fragment_download_queue.xml b/app/src/main/res/layout/fragment_download_queue.xml new file mode 100644 index 000000000..915697712 --- /dev/null +++ b/app/src/main/res/layout/fragment_download_queue.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml new file mode 100644 index 000000000..93167e7b3 --- /dev/null +++ b/app/src/main/res/layout/item_download.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 282e172cc..0c52cefae 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -4,4 +4,5 @@ nav_drawer_recent_updates nav_drawer_catalogues nav_drawer_settings + nav_drawer_downloads \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a084b9b6f..e2a08c581 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,5 +93,6 @@ No new chapters found Found new chapters for: Download threads + Download queue