瀏覽代碼

Initial download queue fragment. Update progress working

inorichi 9 年之前
父節點
當前提交
999cc0df6e

+ 19 - 10
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<Download> 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<Page> 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<Page> 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;
+    }
 }

+ 30 - 0
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<Page> pages;
     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) {
         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<Download> subject) {
+        this.statusSubject = subject;
+    }
+
+    private void notifyStatus() {
+        if (statusSubject != null)
+            statusSubject.onNext(this);
+    }
 }

+ 43 - 0
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<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());
+    }
+
+}

+ 2 - 2
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<Integer> statusSubject;
 

+ 2 - 0
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);

+ 107 - 0
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<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);
+    }
+
+}

+ 13 - 12
app/src/main/java/eu/kanade/mangafeed/presenter/MangaChaptersPresenter.java

@@ -128,7 +128,7 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
                 .toList()
                 .flatMap(db::insertChapters)
                 .observeOn(AndroidSchedulers.mainThread())
-                .doOnCompleted( () -> remove(markReadSubscription) )
+                .doOnCompleted(() -> remove(markReadSubscription))
                 .subscribe(result -> {
                 }));
     }
@@ -137,27 +137,28 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment>
         add(downloadSubscription = selectedChapters
                 .doOnCompleted(() -> remove(downloadSubscription))
                 .subscribe(chapter -> {
-                    EventBus.getDefault().post(
-                            new DownloadChapterEvent(manga, chapter));
+                    EventBus.getDefault().post(new DownloadChapterEvent(manga, chapter));
                 }));
     }
 
+    public void deleteChapters(Observable<Chapter> selectedChapters) {
+        deleteSubscription = selectedChapters
+                .doOnCompleted( () -> remove(deleteSubscription) )
+                .subscribe(chapter -> {
+                    downloadManager.deleteChapter(source, manga, chapter);
+                    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) {
+        if (dir.exists() && dir.listFiles().length > 0 && pageList.exists()) {
             chapter.downloaded = Chapter.DOWNLOADED;
         } else {
             chapter.downloaded = Chapter.NOT_DOWNLOADED;
         }
     }
 
-    public void deleteChapters(Observable<Chapter> selectedChapters) {
-        deleteSubscription = selectedChapters
-                .doOnCompleted( () -> remove(deleteSubscription) )
-                .subscribe(chapter -> {
-                    downloadManager.deleteChapter(source, manga, chapter);
-                    chapter.downloaded = Chapter.NOT_DOWNLOADED;
-                });
-    }
 }

+ 7 - 0
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;

+ 66 - 0
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<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;
+            }
+        }
+    }
+
+}

+ 1 - 2
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<MangaChaptersPresenter
 
     private ActionMode actionMode;
 
-    public static Fragment newInstance() {
+    public static MangaChaptersFragment newInstance() {
         return new MangaChaptersFragment();
     }
 

+ 36 - 0
app/src/main/java/eu/kanade/mangafeed/ui/holder/DownloadHolder.java

@@ -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);
+        }
+    }
+
+}

+ 13 - 0
app/src/main/res/layout/fragment_download_queue.xml

@@ -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>

+ 32 - 0
app/src/main/res/layout/item_download.xml

@@ -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>

+ 1 - 0
app/src/main/res/values/ids.xml

@@ -4,4 +4,5 @@
     <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_settings" type="id">nav_drawer_settings</item>
+    <item name="nav_drawer_downloads" type="id">nav_drawer_downloads</item>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

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