Initial download manager
| @ -46,6 +46,9 @@ | |||||||
|         <service android:name=".data.services.LibraryUpdateService" |         <service android:name=".data.services.LibraryUpdateService" | ||||||
|             android:exported="false"/> |             android:exported="false"/> | ||||||
| 
 | 
 | ||||||
|  |         <service android:name=".data.services.DownloadService" | ||||||
|  |             android:exported="false"/> | ||||||
|  | 
 | ||||||
|         <receiver |         <receiver | ||||||
|             android:name=".data.services.LibraryUpdateService$SyncOnConnectionAvailable" |             android:name=".data.services.LibraryUpdateService$SyncOnConnectionAvailable" | ||||||
|             android:enabled="false"> |             android:enabled="false"> | ||||||
|  | |||||||
| @ -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<DownloadChapterEvent> 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<Page> 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<Page> pages) { | ||||||
|  |         return chapterDir.exists() && chapterDir.listFiles().length == pages.size(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isImageDownloaded(File imagePath) { | ||||||
|  |         return imagePath.exists() && !imagePath.isDirectory(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Observable<Page> getDownloadedImage(final Page page, Source source, File chapterDir) { | ||||||
|  |         Observable<Page> 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<Page> 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<DownloadChapterEvent> getDownloadsSubject() { | ||||||
|  |         return downloadsSubject; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -6,6 +6,7 @@ import android.preference.PreferenceManager; | |||||||
| 
 | 
 | ||||||
| import eu.kanade.mangafeed.R; | import eu.kanade.mangafeed.R; | ||||||
| import eu.kanade.mangafeed.sources.base.Source; | import eu.kanade.mangafeed.sources.base.Source; | ||||||
|  | import eu.kanade.mangafeed.util.DiskUtils; | ||||||
| 
 | 
 | ||||||
| public class PreferencesHelper { | public class PreferencesHelper { | ||||||
| 
 | 
 | ||||||
| @ -53,4 +54,9 @@ public class PreferencesHelper { | |||||||
|                 .apply(); |                 .apply(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public String getDownloadsDirectory() { | ||||||
|  |         return mPref.getString(getKey(R.string.pref_download_directory_key), | ||||||
|  |                 DiskUtils.getStorageDirectories(context)[0]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -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(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -5,6 +5,7 @@ import android.app.Application; | |||||||
| import javax.inject.Singleton; | import javax.inject.Singleton; | ||||||
| 
 | 
 | ||||||
| import dagger.Component; | import dagger.Component; | ||||||
|  | import eu.kanade.mangafeed.data.services.DownloadService; | ||||||
| import eu.kanade.mangafeed.data.services.LibraryUpdateService; | 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; | ||||||
| @ -42,6 +43,7 @@ public interface AppComponent { | |||||||
|     void inject(Source source); |     void inject(Source source); | ||||||
| 
 | 
 | ||||||
|     void inject(LibraryUpdateService libraryUpdateService); |     void inject(LibraryUpdateService libraryUpdateService); | ||||||
|  |     void inject(DownloadService downloadService); | ||||||
| 
 | 
 | ||||||
|     Application application(); |     Application application(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,11 +8,10 @@ import dagger.Module; | |||||||
| import dagger.Provides; | import dagger.Provides; | ||||||
| import eu.kanade.mangafeed.data.caches.CacheManager; | import eu.kanade.mangafeed.data.caches.CacheManager; | ||||||
| import eu.kanade.mangafeed.data.helpers.DatabaseHelper; | 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.NetworkHelper; | ||||||
| import eu.kanade.mangafeed.data.helpers.PreferencesHelper; | import eu.kanade.mangafeed.data.helpers.PreferencesHelper; | ||||||
| import eu.kanade.mangafeed.data.helpers.SourceManager; | 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. |  * Provide dependencies to the DataManager, mainly Helper classes and Retrofit services. | ||||||
| @ -32,12 +31,6 @@ public class DataModule { | |||||||
|         return new DatabaseHelper(app); |         return new DatabaseHelper(app); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Provides |  | ||||||
|     @Singleton |  | ||||||
|     Scheduler provideSubscribeScheduler() { |  | ||||||
|         return Schedulers.io(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Provides |     @Provides | ||||||
|     @Singleton |     @Singleton | ||||||
|     CacheManager provideCacheManager(Application app) { |     CacheManager provideCacheManager(Application app) { | ||||||
| @ -56,4 +49,10 @@ public class DataModule { | |||||||
|         return new SourceManager(app); |         return new SourceManager(app); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     DownloadManager provideDownloadManager(Application app, SourceManager sourceManager) { | ||||||
|  |         return new DownloadManager(app, sourceManager); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| @ -85,6 +85,10 @@ public class MangaChaptersPresenter extends BasePresenter<MangaChaptersFragment> | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public Manga getManga() { | ||||||
|  |         return manga; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public void refreshChapters() { |     public void refreshChapters() { | ||||||
|         if (getView() != null) |         if (getView() != null) | ||||||
|             getView().setSwipeRefreshing(); |             getView().setSwipeRefreshing(); | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ package eu.kanade.mangafeed.sources.base; | |||||||
| import android.content.Context; | import android.content.Context; | ||||||
| 
 | 
 | ||||||
| import com.squareup.okhttp.Headers; | import com.squareup.okhttp.Headers; | ||||||
|  | import com.squareup.okhttp.Response; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @ -123,7 +124,7 @@ public abstract class Source extends BaseSource { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private Observable<Page> cacheImage(final Page page) { |     private Observable<Page> cacheImage(final Page page) { | ||||||
|         return mNetworkService.getProgressResponse(page.getImageUrl(), mRequestHeaders, page) |         return getImageProgressResponse(page) | ||||||
|                 .flatMap(resp -> { |                 .flatMap(resp -> { | ||||||
|                     if (!mCacheManager.putImageToDiskCache(page.getImageUrl(), resp)) { |                     if (!mCacheManager.putImageToDiskCache(page.getImageUrl(), resp)) { | ||||||
|                         throw new IllegalStateException("Unable to save image"); |                         throw new IllegalStateException("Unable to save image"); | ||||||
| @ -132,6 +133,10 @@ public abstract class Source extends BaseSource { | |||||||
|                 }); |                 }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public Observable<Response> getImageProgressResponse(final Page page) { | ||||||
|  |         return mNetworkService.getProgressResponse(page.getImageUrl(), mRequestHeaders, page); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public void savePageList(String chapterUrl, List<Page> pages) { |     public void savePageList(String chapterUrl, List<Page> pages) { | ||||||
|         if (pages != null) |         if (pages != null) | ||||||
|             mCacheManager.putPageUrlsToDiskCache(chapterUrl, pages); |             mCacheManager.putPageUrlsToDiskCache(chapterUrl, pages); | ||||||
|  | |||||||
| @ -18,8 +18,11 @@ import java.util.List; | |||||||
| 
 | 
 | ||||||
| import butterknife.Bind; | import butterknife.Bind; | ||||||
| import butterknife.ButterKnife; | import butterknife.ButterKnife; | ||||||
|  | import de.greenrobot.event.EventBus; | ||||||
| import eu.kanade.mangafeed.R; | import eu.kanade.mangafeed.R; | ||||||
| import eu.kanade.mangafeed.data.models.Chapter; | 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.presenter.MangaChaptersPresenter; | ||||||
| import eu.kanade.mangafeed.ui.activity.MangaDetailActivity; | import eu.kanade.mangafeed.ui.activity.MangaDetailActivity; | ||||||
| import eu.kanade.mangafeed.ui.activity.ReaderActivity; | 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 eu.kanade.mangafeed.ui.fragment.base.BaseRxFragment; | ||||||
| import nucleus.factory.RequiresPresenter; | import nucleus.factory.RequiresPresenter; | ||||||
| import rx.Observable; | import rx.Observable; | ||||||
|  | import rx.Subscription; | ||||||
|  | import rx.schedulers.Schedulers; | ||||||
| 
 | 
 | ||||||
| @RequiresPresenter(MangaChaptersPresenter.class) | @RequiresPresenter(MangaChaptersPresenter.class) | ||||||
| public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter> implements | public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter> implements | ||||||
| @ -39,6 +44,7 @@ public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter | |||||||
|     private ChaptersAdapter adapter; |     private ChaptersAdapter adapter; | ||||||
| 
 | 
 | ||||||
|     private ActionMode actionMode; |     private ActionMode actionMode; | ||||||
|  |     private Subscription downloadSubscription; | ||||||
| 
 | 
 | ||||||
|     public static Fragment newInstance() { |     public static Fragment newInstance() { | ||||||
|         return new MangaChaptersFragment(); |         return new MangaChaptersFragment(); | ||||||
| @ -64,6 +70,15 @@ public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter | |||||||
|         return view; |         return view; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onStart() { | ||||||
|  |         super.onStart(); | ||||||
|  |         if (!DownloadService.isRunning(getActivity())) { | ||||||
|  |             Intent intent = DownloadService.getStartIntent(getActivity()); | ||||||
|  |             getActivity().startService(intent); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||||
|         inflater.inflate(R.menu.chapters, menu); |         inflater.inflate(R.menu.chapters, menu); | ||||||
| @ -130,6 +145,9 @@ public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter | |||||||
|             case R.id.action_mark_as_unread: |             case R.id.action_mark_as_unread: | ||||||
|                 getPresenter().markChaptersRead(getSelectedChapters(), false); |                 getPresenter().markChaptersRead(getSelectedChapters(), false); | ||||||
|                 return true; |                 return true; | ||||||
|  |             case R.id.action_download: | ||||||
|  |                 onDownloadChapters(); | ||||||
|  |                 return true; | ||||||
|         } |         } | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| @ -188,4 +206,20 @@ public class MangaChaptersFragment extends BaseRxFragment<MangaChaptersPresenter | |||||||
|     private void setContextTitle(int count) { |     private void setContextTitle(int count) { | ||||||
|         actionMode.setTitle(getString(R.string.selected_chapters_title, count)); |         actionMode.setTitle(getString(R.string.selected_chapters_title, count)); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void onDownloadChapters() { | ||||||
|  |         if (downloadSubscription != null && !downloadSubscription.isUnsubscribed()) { | ||||||
|  |             downloadSubscription.unsubscribe(); | ||||||
|  |             downloadSubscription = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         downloadSubscription = getSelectedChapters() | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .subscribe(chapter -> { | ||||||
|  |                     EventBus.getDefault().post( | ||||||
|  |                             new DownloadChapterEvent(getPresenter().getManga(), chapter)); | ||||||
|  |                     downloadSubscription.unsubscribe(); | ||||||
|  |                 }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ import okio.BufferedSource; | |||||||
| import okio.Okio; | import okio.Okio; | ||||||
| 
 | 
 | ||||||
| public final class DiskUtils { | public final class DiskUtils { | ||||||
|     private static final Pattern DIR_SEPORATOR = Pattern.compile("/"); |     private static final Pattern DIR_SEPARATOR = Pattern.compile("/"); | ||||||
| 
 | 
 | ||||||
|     private DiskUtils() { |     private DiskUtils() { | ||||||
|         throw new AssertionError(); |         throw new AssertionError(); | ||||||
| @ -58,7 +58,7 @@ public final class DiskUtils { | |||||||
|                     rawUserId = ""; |                     rawUserId = ""; | ||||||
|                 } else { |                 } else { | ||||||
|                     final String path = Environment.getExternalStorageDirectory().getAbsolutePath(); |                     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]; |                     final String lastFolder = folders[folders.length - 1]; | ||||||
|                     boolean isDigit = false; |                     boolean isDigit = false; | ||||||
| 
 | 
 | ||||||
| @ -114,18 +114,17 @@ public final class DiskUtils { | |||||||
|         return sb.toString(); |         return sb.toString(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static File saveBufferedSourceToDirectory(BufferedSource bufferedSource, String directory, String name) throws IOException { |     public static File saveBufferedSourceToDirectory(BufferedSource bufferedSource, File directory, String name) throws IOException { | ||||||
|         File fileDirectory = new File(directory); |         if (!directory.exists()) { | ||||||
|         if (!fileDirectory.exists()) { |             if (!directory.mkdirs()) { | ||||||
|             if (!fileDirectory.mkdirs()) { |  | ||||||
|                 throw new IOException("Failed Creating  Directory"); |                 throw new IOException("Failed Creating  Directory"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         File writeFile = new File(fileDirectory, name); |         File writeFile = new File(directory, name); | ||||||
|         if (writeFile.exists()) { |         if (writeFile.exists()) { | ||||||
|             if (writeFile.delete()) { |             if (writeFile.delete()) { | ||||||
|                 writeFile = new File(fileDirectory, name); |                 writeFile = new File(directory, name); | ||||||
|             } else { |             } else { | ||||||
|                 throw new IOException("Failed Deleting Existing File for Overwrite"); |                 throw new IOException("Failed Deleting Existing File for Overwrite"); | ||||||
|             } |             } | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 176 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-ldpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 250 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 131 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-tvdpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 482 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 178 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 232 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_file_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 307 B | 
| @ -3,6 +3,11 @@ | |||||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> |     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||||
| 
 | 
 | ||||||
|  |     <item android:id="@+id/action_download" | ||||||
|  |         android:title="@string/action_download" | ||||||
|  |         android:icon="@drawable/ic_file_download" | ||||||
|  |         app:showAsAction="ifRoom"/> | ||||||
|  | 
 | ||||||
|     <item android:id="@+id/action_mark_as_read" |     <item android:id="@+id/action_mark_as_read" | ||||||
|         android:title="@string/action_mark_as_read" |         android:title="@string/action_mark_as_read" | ||||||
|         android:icon="@drawable/ic_action_done_all" |         android:icon="@drawable/ic_action_done_all" | ||||||
|  | |||||||
| @ -4,4 +4,5 @@ | |||||||
|     <string name="pref_category_accounts_key">pref_category_accounts_key</string> |     <string name="pref_category_accounts_key">pref_category_accounts_key</string> | ||||||
|     <string name="pref_fullscreen_key">pref_fullscreen_key</string> |     <string name="pref_fullscreen_key">pref_fullscreen_key</string> | ||||||
|     <string name="pref_default_viewer_key">pref_default_viewer_key</string> |     <string name="pref_default_viewer_key">pref_default_viewer_key</string> | ||||||
|  |     <string name="pref_download_directory_key">pref_download_directory_key</string> | ||||||
| </resources> | </resources> | ||||||
| @ -86,6 +86,7 @@ | |||||||
|     <string name="notification_completed">Update completed</string> |     <string name="notification_completed">Update completed</string> | ||||||
|     <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="action_download">Download</string> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| </resources> | </resources> | ||||||
|  | |||||||
| @ -1,85 +0,0 @@ | |||||||
| package eu.kanade.mangafeed; |  | ||||||
| 
 |  | ||||||
| import android.os.Build; |  | ||||||
| 
 |  | ||||||
| import junit.framework.Assert; |  | ||||||
| 
 |  | ||||||
| import org.junit.Before; |  | ||||||
| import org.junit.Test; |  | ||||||
| import org.junit.runner.RunWith; |  | ||||||
| import org.robolectric.RobolectricGradleTestRunner; |  | ||||||
| import org.robolectric.RuntimeEnvironment; |  | ||||||
| import org.robolectric.annotation.Config; |  | ||||||
| 
 |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| import eu.kanade.mangafeed.data.caches.CacheManager; |  | ||||||
| import eu.kanade.mangafeed.data.helpers.NetworkHelper; |  | ||||||
| import eu.kanade.mangafeed.data.models.Chapter; |  | ||||||
| import eu.kanade.mangafeed.data.models.Manga; |  | ||||||
| import eu.kanade.mangafeed.sources.Batoto; |  | ||||||
| import eu.kanade.mangafeed.sources.base.Source; |  | ||||||
| 
 |  | ||||||
| @Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP) |  | ||||||
| @RunWith(RobolectricGradleTestRunner.class) |  | ||||||
| public class BatotoTest { |  | ||||||
| 
 |  | ||||||
|     NetworkHelper net; |  | ||||||
|     CacheManager cache; |  | ||||||
|     Source b; |  | ||||||
|     final String chapterUrl ="http://bato.to/read/_/345144/minamoto-kun-monogatari_ch178_by_vortex-scans"; |  | ||||||
|     final String mangaUrl = "http://bato.to/comic/_/comics/natsuzora-and-run-r9597"; |  | ||||||
|     final String mangaUrl2 = "http://bato.to/comic/_/comics/bungaku-shoujo-to-shinitagari-no-pierrot-r534"; |  | ||||||
|     final String nisekoiUrl = "http://bato.to/comic/_/comics/nisekoi-r951"; |  | ||||||
| 
 |  | ||||||
|     @Before |  | ||||||
|     public void setUp() { |  | ||||||
|         net = new NetworkHelper(); |  | ||||||
|         cache = new CacheManager(RuntimeEnvironment.application.getApplicationContext()); |  | ||||||
|         b = new Batoto(net, cache); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testImageList() { |  | ||||||
|         List<String> imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl) |  | ||||||
|                 .toList().toBlocking().single(); |  | ||||||
| 
 |  | ||||||
|         Assert.assertTrue(imageUrls.size() > 5); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testMangaList() { |  | ||||||
|         List<Manga> mangaList = b.pullPopularMangasFromNetwork(1) |  | ||||||
|                 .toBlocking().first(); |  | ||||||
| 
 |  | ||||||
|         Manga m = mangaList.get(0); |  | ||||||
|         Assert.assertNotNull(m.title); |  | ||||||
|         Assert.assertNotNull(m.artist); |  | ||||||
|         Assert.assertNotNull(m.author); |  | ||||||
|         Assert.assertNotNull(m.url); |  | ||||||
| 
 |  | ||||||
|         Assert.assertTrue(mangaList.size() > 25); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testChapterList() { |  | ||||||
|         List<Chapter> mangaList = b.pullChaptersFromNetwork(mangaUrl) |  | ||||||
|                 .toBlocking().first(); |  | ||||||
| 
 |  | ||||||
|         Assert.assertTrue(mangaList.size() > 5); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testMangaDetails() { |  | ||||||
|         Manga nisekoi = b.pullMangaFromNetwork(nisekoiUrl) |  | ||||||
|                 .toBlocking().single(); |  | ||||||
| 
 |  | ||||||
|         Assert.assertEquals("Nisekoi", nisekoi.title); |  | ||||||
|         Assert.assertEquals("Komi Naoshi", nisekoi.author); |  | ||||||
|         Assert.assertEquals("Komi Naoshi", nisekoi.artist); |  | ||||||
|         Assert.assertEquals("http://bato.to/comic/_/nisekoi-r951", nisekoi.url); |  | ||||||
|         Assert.assertEquals("http://img.bato.to/forums/uploads/a2a850c644a50bccc462f36922c1cbf2.jpg", nisekoi.thumbnail_url); |  | ||||||
|         Assert.assertTrue(nisekoi.description.length() > 20); |  | ||||||
|         Assert.assertTrue(nisekoi.genre.length() > 20); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,80 +0,0 @@ | |||||||
| package eu.kanade.mangafeed; |  | ||||||
| 
 |  | ||||||
| import android.os.Build; |  | ||||||
| 
 |  | ||||||
| import junit.framework.Assert; |  | ||||||
| 
 |  | ||||||
| import org.junit.Before; |  | ||||||
| import org.junit.Test; |  | ||||||
| import org.junit.runner.RunWith; |  | ||||||
| import org.robolectric.RobolectricGradleTestRunner; |  | ||||||
| import org.robolectric.RuntimeEnvironment; |  | ||||||
| import org.robolectric.annotation.Config; |  | ||||||
| 
 |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| import eu.kanade.mangafeed.data.caches.CacheManager; |  | ||||||
| import eu.kanade.mangafeed.data.helpers.NetworkHelper; |  | ||||||
| import eu.kanade.mangafeed.data.models.Chapter; |  | ||||||
| import eu.kanade.mangafeed.data.models.Manga; |  | ||||||
| import eu.kanade.mangafeed.sources.MangaHere; |  | ||||||
| import eu.kanade.mangafeed.sources.base.Source; |  | ||||||
| 
 |  | ||||||
| @Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP) |  | ||||||
| @RunWith(RobolectricGradleTestRunner.class) |  | ||||||
| public class MangahereTest { |  | ||||||
| 
 |  | ||||||
|     NetworkHelper net; |  | ||||||
|     CacheManager cache; |  | ||||||
|     Source b; |  | ||||||
|     final String chapterUrl ="http://www.mangahere.co/manga/kimi_ni_todoke/v15/c099/"; |  | ||||||
|     final String mangaUrl = "http://www.mangahere.co/manga/kimi_ni_todoke/"; |  | ||||||
| 
 |  | ||||||
|     @Before |  | ||||||
|     public void setUp() { |  | ||||||
|         net = new NetworkHelper(); |  | ||||||
|         cache = new CacheManager(RuntimeEnvironment.application.getApplicationContext()); |  | ||||||
|         b = new MangaHere(net, cache); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testImageList() { |  | ||||||
|         List<String> imageUrls = b.getRemainingImageUrlsFromPageList(chapterUrl) |  | ||||||
|                 .toList().toBlocking().single(); |  | ||||||
| 
 |  | ||||||
|         Assert.assertTrue(imageUrls.size() > 5); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testMangaList() { |  | ||||||
|         List<Manga> mangaList = b.pullPopularMangasFromNetwork(1) |  | ||||||
|                 .toBlocking().first(); |  | ||||||
| 
 |  | ||||||
|         Manga m = mangaList.get(0); |  | ||||||
|         Assert.assertNotNull(m.title); |  | ||||||
|         Assert.assertNotNull(m.url); |  | ||||||
| 
 |  | ||||||
|         Assert.assertTrue(mangaList.size() > 25); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testChapterList() { |  | ||||||
|         List<Chapter> mangaList = b.pullChaptersFromNetwork(mangaUrl) |  | ||||||
|                 .toBlocking().first(); |  | ||||||
| 
 |  | ||||||
|         Assert.assertTrue(mangaList.size() > 5); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     public void testMangaDetails() { |  | ||||||
|         Manga manga = b.pullMangaFromNetwork(mangaUrl) |  | ||||||
|                 .toBlocking().single(); |  | ||||||
| 
 |  | ||||||
|         Assert.assertEquals("Shiina Karuho", manga.author); |  | ||||||
|         Assert.assertEquals("Shiina Karuho", manga.artist); |  | ||||||
|         Assert.assertEquals("http://www.mangahere.co/manga/kimi_ni_todoke/", manga.url); |  | ||||||
|         Assert.assertEquals("http://a.mhcdn.net/store/manga/4999/cover.jpg?v=1433950383", manga.thumbnail_url); |  | ||||||
|         Assert.assertTrue(manga.description.length() > 20); |  | ||||||
|         Assert.assertTrue(manga.genre.length() > 20); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
 inorichi
						inorichi