Initial download queue fragment. Update progress working

This commit is contained in:
inorichi 2015-11-04 18:54:07 +01:00
parent 3b9f4cb6f1
commit 999cc0df6e
15 changed files with 374 additions and 27 deletions

View File

@ -12,11 +12,11 @@ import java.io.FileOutputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import eu.kanade.mangafeed.data.models.Chapter; import eu.kanade.mangafeed.data.models.Chapter;
import eu.kanade.mangafeed.data.models.Download; 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.Manga;
import eu.kanade.mangafeed.data.models.Page; import eu.kanade.mangafeed.data.models.Page;
import eu.kanade.mangafeed.events.DownloadChapterEvent; import eu.kanade.mangafeed.events.DownloadChapterEvent;
@ -37,7 +37,9 @@ public class DownloadManager {
private PreferencesHelper preferences; private PreferencesHelper preferences;
private Gson gson; private Gson gson;
private List<Download> queue; private DownloadQueue queue;
public static final String PAGE_LIST_FILE = "index.json";
public DownloadManager(Context context, SourceManager sourceManager, PreferencesHelper preferences) { public DownloadManager(Context context, SourceManager sourceManager, PreferencesHelper preferences) {
this.context = context; this.context = context;
@ -45,7 +47,7 @@ public class DownloadManager {
this.preferences = preferences; this.preferences = preferences;
this.gson = new Gson(); this.gson = new Gson();
queue = new ArrayList<>(); queue = new DownloadQueue();
initializeDownloadSubscription(); initializeDownloadSubscription();
} }
@ -78,7 +80,7 @@ public class DownloadManager {
final Source source = sourceManager.get(event.getManga().source); final Source source = sourceManager.get(event.getManga().source);
// If the chapter is already queued, don't add it again // 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) if (download.chapter.id == event.getChapter().id)
return true; return true;
} }
@ -119,7 +121,10 @@ public class DownloadManager {
.pullPageListFromNetwork(download.chapter.url) .pullPageListFromNetwork(download.chapter.url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
// Add resulting pages to download object // 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 // Get all the URLs to the source images, fetch pages if necessary
.flatMap(pageList -> Observable.merge( .flatMap(pageList -> Observable.merge(
Observable.from(pageList).filter(page -> page.getImageUrl() != null), 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 // Start downloading images, consider we can have downloaded images already
.concatMap(page -> getDownloadedImage(page, download.source, download.directory)) .concatMap(page -> getDownloadedImage(page, download.source, download.directory))
// Remove from the queue // Remove from the queue
.doOnCompleted(() -> removeFromQueue(download)); .doOnCompleted(() -> onChapterDownloaded(download));
} }
// Get downloaded image if exists, otherwise download it with the method below // Get downloaded image if exists, otherwise download it with the method below
@ -179,15 +184,15 @@ public class DownloadManager {
return imagePath.exists() && !imagePath.isDirectory(); 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); 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 // Return the page list from the chapter's directory if it exists, null otherwise
public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) { public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) {
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter); File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, "index.json"); File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
try { try {
JsonReader reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath())); JsonReader reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath()));
@ -202,7 +207,7 @@ public class DownloadManager {
// Save the page list to the chapter's directory // Save the page list to the chapter's directory
public void savePageList(Source source, Manga manga, Chapter chapter, List<Page> pages) { public void savePageList(Source source, Manga manga, Chapter chapter, List<Page> pages) {
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter); File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, "index.json"); File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
FileOutputStream out; FileOutputStream out;
try { try {
@ -230,4 +235,8 @@ public class DownloadManager {
File path = getAbsoluteChapterDirectory(source, manga, chapter); File path = getAbsoluteChapterDirectory(source, manga, chapter);
DiskUtils.deleteFiles(path); DiskUtils.deleteFiles(path);
} }
public DownloadQueue getQueue() {
return queue;
}
} }

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.util.List; import java.util.List;
import eu.kanade.mangafeed.sources.base.Source; import eu.kanade.mangafeed.sources.base.Source;
import rx.subjects.PublishSubject;
public class Download { public class Download {
public Source source; public Source source;
@ -12,9 +13,38 @@ public class Download {
public List<Page> pages; public List<Page> pages;
public File directory; public File directory;
public transient volatile int totalProgress;
private transient volatile int status;
private transient PublishSubject<Download> 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) { public Download(Source source, Manga manga, Chapter chapter) {
this.source = source; this.source = source;
this.manga = manga; this.manga = manga;
this.chapter = chapter; this.chapter = chapter;
} }
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
notifyStatus();
}
public void setStatusSubject(PublishSubject<Download> subject) {
this.statusSubject = subject;
}
private void notifyStatus() {
if (statusSubject != null)
statusSubject.onNext(this);
}
} }

View File

@ -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<Download> queue;
private PublishSubject<Download> 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<Download> get() {
return queue;
}
public Observable<Download> getActiveDownloads() {
return Observable.from(queue)
.filter(download -> download.getStatus() == Download.DOWNLOADING);
}
public Observable<Download> getStatusObservable() {
return statusSubject
.startWith(getActiveDownloads());
}
}

View File

@ -9,8 +9,8 @@ public class Page implements NetworkHelper.ProgressListener {
private String url; private String url;
private String imageUrl; private String imageUrl;
private String imagePath; private String imagePath;
private transient int status; private transient volatile int status;
private transient int progress; private transient volatile int progress;
private transient BehaviorSubject<Integer> statusSubject; private transient BehaviorSubject<Integer> statusSubject;

View File

@ -10,6 +10,7 @@ import eu.kanade.mangafeed.data.services.LibraryUpdateService;
import eu.kanade.mangafeed.injection.module.AppModule; import eu.kanade.mangafeed.injection.module.AppModule;
import eu.kanade.mangafeed.injection.module.DataModule; import eu.kanade.mangafeed.injection.module.DataModule;
import eu.kanade.mangafeed.presenter.CataloguePresenter; import eu.kanade.mangafeed.presenter.CataloguePresenter;
import eu.kanade.mangafeed.presenter.DownloadQueuePresenter;
import eu.kanade.mangafeed.presenter.LibraryPresenter; import eu.kanade.mangafeed.presenter.LibraryPresenter;
import eu.kanade.mangafeed.presenter.MangaChaptersPresenter; import eu.kanade.mangafeed.presenter.MangaChaptersPresenter;
import eu.kanade.mangafeed.presenter.MangaDetailPresenter; import eu.kanade.mangafeed.presenter.MangaDetailPresenter;
@ -37,6 +38,7 @@ public interface AppComponent {
void inject(MangaInfoPresenter mangaInfoPresenter); void inject(MangaInfoPresenter mangaInfoPresenter);
void inject(MangaChaptersPresenter mangaChaptersPresenter); void inject(MangaChaptersPresenter mangaChaptersPresenter);
void inject(ReaderPresenter readerPresenter); void inject(ReaderPresenter readerPresenter);
void inject(DownloadQueuePresenter downloadQueuePresenter);
void inject(ReaderActivity readerActivity); void inject(ReaderActivity readerActivity);
void inject(SettingsAccountsFragment settingsAccountsFragment); void inject(SettingsAccountsFragment settingsAccountsFragment);

View File

@ -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<DownloadQueueFragment> {
@Inject DownloadManager downloadManager;
private DownloadQueue downloadQueue;
private Subscription statusSubscription;
private HashMap<Download, Subscription> 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);
}
}

View File

@ -137,21 +137,10 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
add(downloadSubscription = selectedChapters add(downloadSubscription = selectedChapters
.doOnCompleted(() -> remove(downloadSubscription)) .doOnCompleted(() -> remove(downloadSubscription))
.subscribe(chapter -> { .subscribe(chapter -> {
EventBus.getDefault().post( EventBus.getDefault().post(new DownloadChapterEvent(manga, chapter));
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<Chapter> selectedChapters) { public void deleteChapters(Observable<Chapter> selectedChapters) {
deleteSubscription = selectedChapters deleteSubscription = selectedChapters
.doOnCompleted( () -> remove(deleteSubscription) ) .doOnCompleted( () -> remove(deleteSubscription) )
@ -160,4 +149,16 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
chapter.downloaded = Chapter.NOT_DOWNLOADED; 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;
}
}
} }

View File

@ -15,6 +15,7 @@ import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.ui.activity.base.BaseActivity; 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.LibraryFragment;
import eu.kanade.mangafeed.ui.fragment.SourceFragment; import eu.kanade.mangafeed.ui.fragment.SourceFragment;
@ -51,6 +52,9 @@ public class MainActivity extends BaseActivity {
new PrimaryDrawerItem() new PrimaryDrawerItem()
.withName(R.string.catalogues_title) .withName(R.string.catalogues_title)
.withIdentifier(R.id.nav_drawer_catalogues), .withIdentifier(R.id.nav_drawer_catalogues),
new PrimaryDrawerItem()
.withName(R.string.download_title)
.withIdentifier(R.id.nav_drawer_downloads),
new PrimaryDrawerItem() new PrimaryDrawerItem()
.withName(R.string.settings_title) .withName(R.string.settings_title)
.withIdentifier(R.id.nav_drawer_settings) .withIdentifier(R.id.nav_drawer_settings)
@ -70,6 +74,9 @@ public class MainActivity extends BaseActivity {
case R.id.nav_drawer_catalogues: case R.id.nav_drawer_catalogues:
setFragment(SourceFragment.newInstance()); setFragment(SourceFragment.newInstance());
break; break;
case R.id.nav_drawer_downloads:
setFragment(DownloadQueueFragment.newInstance());
break;
case R.id.nav_drawer_settings: case R.id.nav_drawer_settings:
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
break; break;

View File

@ -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<DownloadQueuePresenter> {
@Bind(R.id.download_list) RecyclerView downloadList;
private EasyRecyclerAdapter<Download> 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<Download> 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;
}
}
}
}

View File

@ -2,7 +2,6 @@ package eu.kanade.mangafeed.ui.fragment;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.view.ActionMode; import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@ -41,7 +40,7 @@ public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter
private ActionMode actionMode; private ActionMode actionMode;
public static Fragment newInstance() { public static MangaChaptersFragment newInstance() {
return new MangaChaptersFragment(); return new MangaChaptersFragment();
} }

View File

@ -0,0 +1,36 @@
package eu.kanade.mangafeed.ui.holder;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.models.Download;
import uk.co.ribot.easyadapter.ItemViewHolder;
import uk.co.ribot.easyadapter.PositionInfo;
import uk.co.ribot.easyadapter.annotations.LayoutId;
import uk.co.ribot.easyadapter.annotations.ViewId;
@LayoutId(R.layout.item_download)
public class DownloadHolder extends ItemViewHolder<Download> {
@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);
}
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/download_list">
</android.support.v7.widget.RecyclerView>
</LinearLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:id="@+id/download_title"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/download_progress" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/download_progress_text"
android:layout_gravity="center_horizontal" />
</FrameLayout>
</LinearLayout>

View File

@ -4,4 +4,5 @@
<item name="nav_drawer_recent_updates" type="id">nav_drawer_recent_updates</item> <item name="nav_drawer_recent_updates" type="id">nav_drawer_recent_updates</item>
<item name="nav_drawer_catalogues" type="id">nav_drawer_catalogues</item> <item name="nav_drawer_catalogues" type="id">nav_drawer_catalogues</item>
<item name="nav_drawer_settings" type="id">nav_drawer_settings</item> <item name="nav_drawer_settings" type="id">nav_drawer_settings</item>
<item name="nav_drawer_downloads" type="id">nav_drawer_downloads</item>
</resources> </resources>

View File

@ -93,5 +93,6 @@
<string name="notification_no_new_chapters">No new chapters found</string> <string name="notification_no_new_chapters">No new chapters found</string>
<string name="notification_new_chapters">Found new chapters for:</string> <string name="notification_new_chapters">Found new chapters for:</string>
<string name="pref_download_threads">Download threads</string> <string name="pref_download_threads">Download threads</string>
<string name="download_title">Download queue</string>
</resources> </resources>