diff --git a/app/build.gradle b/app/build.gradle
index 3c59b64b8..3ad59e14e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'me.tatarka.retrolambda'
@@ -80,6 +81,13 @@ android {
checkReleaseBuilds false
}
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ // http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
+ useLibrary 'org.apache.http.legacy'
+
}
apt {
@@ -92,7 +100,8 @@ dependencies {
final SUPPORT_LIBRARY_VERSION = '23.1.1'
final DAGGER_VERSION = '2.0.2'
final EVENTBUS_VERSION = '3.0.0'
- final OKHTTP_VERSION = '3.1.1'
+ final OKHTTP_VERSION = '3.1.2'
+ final RETROFIT_VERSION = '2.0.0-beta4'
final STORIO_VERSION = '1.8.0'
final ICEPICK_VERSION = '3.1.0'
final MOCKITO_VERSION = '1.10.19'
@@ -111,20 +120,22 @@ dependencies {
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
compile 'com.squareup.okio:okio:1.6.0'
- compile 'com.google.code.gson:gson:2.5'
+ compile 'com.google.code.gson:gson:2.6.1'
compile 'com.jakewharton:disklrucache:2.0.2'
compile 'org.jsoup:jsoup:1.8.3'
compile 'io.reactivex:rxandroid:1.1.0'
- compile 'io.reactivex:rxjava:1.1.0'
- compile 'com.squareup.retrofit:retrofit:1.9.0'
+ compile 'io.reactivex:rxjava:1.1.1'
+ compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
+ compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
+ compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
- compile 'info.android15.nucleus:nucleus:2.0.4'
- compile 'com.github.bumptech.glide:glide:3.6.1'
+ compile 'info.android15.nucleus:nucleus:2.0.5'
+ compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.jakewharton.timber:timber:4.1.0'
- compile 'ch.acra:acra:4.8.1'
+ compile 'ch.acra:acra:4.8.2'
compile "frankiesardo:icepick:$ICEPICK_VERSION"
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
@@ -161,4 +172,19 @@ dependencies {
}
androidTestApt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+}
+
+buildscript {
+ ext.kotlin_version = '1.0.0'
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+repositories {
+ mavenCentral()
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 873e9f402..ed8a82fa7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -51,17 +51,17 @@
android:theme="@style/FilePickerTheme">
-
-
@@ -69,7 +69,7 @@
+ android:name=".data.library.LibraryUpdateAlarm">
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.java b/app/src/main/java/eu/kanade/tachiyomi/App.java
index 63dfa0cc7..3992bffd4 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/App.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.java
@@ -33,16 +33,18 @@ public class App extends Application {
super.onCreate();
if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree());
- applicationComponent = DaggerAppComponent.builder()
- .appModule(new AppModule(this))
- .build();
+ applicationComponent = prepareAppComponent().build();
componentInjector =
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
setupEventBus();
+ setupAcra();
+ }
- ACRA.init(this);
+ protected DaggerAppComponent.Builder prepareAppComponent() {
+ return DaggerAppComponent.builder()
+ .appModule(new AppModule(this));
}
protected void setupEventBus() {
@@ -52,13 +54,12 @@ public class App extends Application {
.installDefaultEventBus();
}
- public AppComponent getComponent() {
- return applicationComponent;
+ protected void setupAcra() {
+ ACRA.init(this);
}
- // Needed to replace the component with a test specific one
- public void setComponent(AppComponent applicationComponent) {
- this.applicationComponent = applicationComponent;
+ public AppComponent getComponent() {
+ return applicationComponent;
}
public ComponentReflectionInjector getComponentReflection() {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.java b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.java
deleted file mode 100644
index 41b46df34..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.java
+++ /dev/null
@@ -1,268 +0,0 @@
-package eu.kanade.tachiyomi.data.cache;
-
-import android.content.Context;
-import android.text.format.Formatter;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.jakewharton.disklrucache.DiskLruCache;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.reflect.Type;
-import java.util.List;
-
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.util.DiskUtils;
-import okhttp3.Response;
-import okio.BufferedSink;
-import okio.Okio;
-import rx.Observable;
-
-/**
- * Class used to create chapter cache
- * For each image in a chapter a file is created
- * For each chapter a Json list is created and converted to a file.
- * The files are in format *md5key*.0
- */
-public class ChapterCache {
-
- /** Name of cache directory. */
- private static final String PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache";
-
- /** Application cache version. */
- private static final int PARAMETER_APP_VERSION = 1;
-
- /** The number of values per cache entry. Must be positive. */
- private static final int PARAMETER_VALUE_COUNT = 1;
-
- /** The maximum number of bytes this cache should use to store. */
- private static final int PARAMETER_CACHE_SIZE = 75 * 1024 * 1024;
-
- /** Interface to global information about an application environment. */
- private final Context context;
-
- /** Google Json class used for parsing JSON files. */
- private final Gson gson;
-
- /** Cache class used for cache management. */
- private DiskLruCache diskCache;
-
- /** Page list collection used for deserializing from JSON. */
- private final Type pageListCollection;
-
- /**
- * Constructor of ChapterCache.
- * @param context application environment interface.
- */
- public ChapterCache(Context context) {
- this.context = context;
-
- // Initialize Json handler.
- gson = new Gson();
-
- // Try to open cache in default cache directory.
- try {
- diskCache = DiskLruCache.open(
- new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY),
- PARAMETER_APP_VERSION,
- PARAMETER_VALUE_COUNT,
- PARAMETER_CACHE_SIZE
- );
- } catch (IOException e) {
- // Do Nothing.
- }
-
- pageListCollection = new TypeToken>() {}.getType();
- }
-
- /**
- * Returns directory of cache.
- * @return directory of cache.
- */
- public File getCacheDir() {
- return diskCache.getDirectory();
- }
-
- /**
- * Returns real size of directory.
- * @return real size of directory.
- */
- private long getRealSize() {
- return DiskUtils.getDirectorySize(getCacheDir());
- }
-
- /**
- * Returns real size of directory in human readable format.
- * @return real size of directory.
- */
- public String getReadableSize() {
- return Formatter.formatFileSize(context, getRealSize());
- }
-
- /**
- * Remove file from cache.
- * @param file name of file "md5.0".
- * @return status of deletion for the file.
- */
- public boolean removeFileFromCache(String file) {
- // Make sure we don't delete the journal file (keeps track of cache).
- if (file.equals("journal") || file.startsWith("journal."))
- return false;
-
- try {
- // Remove the extension from the file to get the key of the cache
- String key = file.substring(0, file.lastIndexOf("."));
- // Remove file from cache.
- return diskCache.remove(key);
- } catch (IOException e) {
- return false;
- }
- }
-
- /**
- * Get page list from cache.
- * @param chapterUrl the url of the chapter.
- * @return an observable of the list of pages.
- */
- public Observable> getPageListFromCache(final String chapterUrl) {
- return Observable.fromCallable(() -> {
- // Initialize snapshot (a snapshot of the values for an entry).
- DiskLruCache.Snapshot snapshot = null;
-
- try {
- // Create md5 key and retrieve snapshot.
- String key = DiskUtils.hashKeyForDisk(chapterUrl);
- snapshot = diskCache.get(key);
-
- // Convert JSON string to list of objects.
- return gson.fromJson(snapshot.getString(0), pageListCollection);
-
- } finally {
- if (snapshot != null) {
- snapshot.close();
- }
- }
- });
- }
-
- /**
- * Add page list to disk cache.
- * @param chapterUrl the url of the chapter.
- * @param pages list of pages.
- */
- public void putPageListToCache(final String chapterUrl, final List pages) {
- // Convert list of pages to json string.
- String cachedValue = gson.toJson(pages);
-
- // Initialize the editor (edits the values for an entry).
- DiskLruCache.Editor editor = null;
-
- // Initialize OutputStream.
- OutputStream outputStream = null;
-
- try {
- // Get editor from md5 key.
- String key = DiskUtils.hashKeyForDisk(chapterUrl);
- editor = diskCache.edit(key);
- if (editor == null) {
- return;
- }
-
- // Write chapter urls to cache.
- outputStream = new BufferedOutputStream(editor.newOutputStream(0));
- outputStream.write(cachedValue.getBytes());
- outputStream.flush();
-
- diskCache.flush();
- editor.commit();
- } catch (Exception e) {
- // Do Nothing.
- } finally {
- if (editor != null) {
- editor.abortUnlessCommitted();
- }
- if (outputStream != null) {
- try {
- outputStream.close();
- } catch (IOException ignore) {
- // Do Nothing.
- }
- }
- }
- }
-
- /**
- * Check if image is in cache.
- * @param imageUrl url of image.
- * @return true if in cache otherwise false.
- */
- public boolean isImageInCache(final String imageUrl) {
- try {
- return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null;
- } catch (IOException e) {
- return false;
- }
- }
-
- /**
- * Get image path from url.
- * @param imageUrl url of image.
- * @return path of image.
- */
- public String getImagePath(final String imageUrl) {
- try {
- // Get file from md5 key.
- String imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0";
- File file = new File(diskCache.getDirectory(), imageName);
- return file.getCanonicalPath();
- } catch (IOException e) {
- return null;
- }
- }
-
- /**
- * Add image to cache.
- * @param imageUrl url of image.
- * @param response http response from page.
- * @throws IOException image error.
- */
- public void putImageToCache(final String imageUrl, final Response response) throws IOException {
- // Initialize editor (edits the values for an entry).
- DiskLruCache.Editor editor = null;
-
- // Initialize BufferedSink (used for small writes).
- BufferedSink sink = null;
-
- try {
- // Get editor from md5 key.
- String key = DiskUtils.hashKeyForDisk(imageUrl);
- editor = diskCache.edit(key);
- if (editor == null) {
- throw new IOException("Unable to edit key");
- }
-
- // Initialize OutputStream and write image.
- OutputStream outputStream = new BufferedOutputStream(editor.newOutputStream(0));
- sink = Okio.buffer(Okio.sink(outputStream));
- sink.writeAll(response.body().source());
-
- diskCache.flush();
- editor.commit();
- } catch (Exception e) {
- response.body().close();
- throw new IOException("Unable to save image");
- } finally {
- if (editor != null) {
- editor.abortUnlessCommitted();
- }
- if (sink != null) {
- sink.close();
- }
- }
- }
-
-}
-
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
new file mode 100644
index 000000000..1ff58e6f3
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
@@ -0,0 +1,213 @@
+package eu.kanade.tachiyomi.data.cache
+
+import android.content.Context
+import android.text.format.Formatter
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.jakewharton.disklrucache.DiskLruCache
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.util.DiskUtils
+import okhttp3.Response
+import okio.Okio
+import rx.Observable
+import java.io.File
+import java.io.IOException
+import java.lang.reflect.Type
+
+/**
+ * Class used to create chapter cache
+ * For each image in a chapter a file is created
+ * For each chapter a Json list is created and converted to a file.
+ * The files are in format *md5key*.0
+ *
+ * @param context the application context.
+ * @constructor creates an instance of the chapter cache.
+ */
+class ChapterCache(private val context: Context) {
+
+ /** Google Json class used for parsing JSON files. */
+ private val gson: Gson = Gson()
+
+ /** Cache class used for cache management. */
+ private val diskCache: DiskLruCache
+
+ /** Page list collection used for deserializing from JSON. */
+ private val pageListCollection: Type = object : TypeToken>() {}.type
+
+ companion object {
+ /** Name of cache directory. */
+ const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
+
+ /** Application cache version. */
+ const val PARAMETER_APP_VERSION = 1
+
+ /** The number of values per cache entry. Must be positive. */
+ const val PARAMETER_VALUE_COUNT = 1
+
+ /** The maximum number of bytes this cache should use to store. */
+ const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
+ }
+
+ init {
+ // Open cache in default cache directory.
+ diskCache = DiskLruCache.open(
+ File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
+ PARAMETER_APP_VERSION,
+ PARAMETER_VALUE_COUNT,
+ PARAMETER_CACHE_SIZE)
+ }
+
+ /**
+ * Returns directory of cache.
+ * @return directory of cache.
+ */
+ val cacheDir: File
+ get() = diskCache.directory
+
+ /**
+ * Returns real size of directory.
+ * @return real size of directory.
+ */
+ private val realSize: Long
+ get() = DiskUtils.getDirectorySize(cacheDir)
+
+ /**
+ * Returns real size of directory in human readable format.
+ * @return real size of directory.
+ */
+ val readableSize: String
+ get() = Formatter.formatFileSize(context, realSize)
+
+ /**
+ * Remove file from cache.
+ * @param file name of file "md5.0".
+ * @return status of deletion for the file.
+ */
+ fun removeFileFromCache(file: String): Boolean {
+ // Make sure we don't delete the journal file (keeps track of cache).
+ if (file == "journal" || file.startsWith("journal."))
+ return false
+
+ try {
+ // Remove the extension from the file to get the key of the cache
+ val key = file.substring(0, file.lastIndexOf("."))
+ // Remove file from cache.
+ return diskCache.remove(key)
+ } catch (e: IOException) {
+ return false
+ }
+ }
+
+ /**
+ * Get page list from cache.
+ * @param chapterUrl the url of the chapter.
+ * @return an observable of the list of pages.
+ */
+ fun getPageListFromCache(chapterUrl: String): Observable> {
+ return Observable.fromCallable> {
+ // Get the key for the chapter.
+ val key = DiskUtils.hashKeyForDisk(chapterUrl)
+
+ // Convert JSON string to list of objects. Throws an exception if snapshot is null
+ diskCache.get(key).use {
+ gson.fromJson(it.getString(0), pageListCollection)
+ }
+ }
+ }
+
+ /**
+ * Add page list to disk cache.
+ * @param chapterUrl the url of the chapter.
+ * @param pages list of pages.
+ */
+ fun putPageListToCache(chapterUrl: String, pages: List) {
+ // Convert list of pages to json string.
+ val cachedValue = gson.toJson(pages)
+
+ // Initialize the editor (edits the values for an entry).
+ var editor: DiskLruCache.Editor? = null
+
+ try {
+ // Get editor from md5 key.
+ val key = DiskUtils.hashKeyForDisk(chapterUrl)
+ editor = diskCache.edit(key) ?: return
+
+ // Write chapter urls to cache.
+ Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
+ it.write(cachedValue.toByteArray())
+ it.flush()
+ }
+
+ diskCache.flush()
+ editor.commit()
+ editor.abortUnlessCommitted()
+
+ } catch (e: Exception) {
+ // Ignore.
+ } finally {
+ editor?.abortUnlessCommitted()
+ }
+ }
+
+ /**
+ * Check if image is in cache.
+ * @param imageUrl url of image.
+ * @return true if in cache otherwise false.
+ */
+ fun isImageInCache(imageUrl: String): Boolean {
+ try {
+ return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null
+ } catch (e: IOException) {
+ return false
+ }
+ }
+
+ /**
+ * Get image path from url.
+ * @param imageUrl url of image.
+ * @return path of image.
+ */
+ fun getImagePath(imageUrl: String): String? {
+ try {
+ // Get file from md5 key.
+ val imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0"
+ return File(diskCache.directory, imageName).canonicalPath
+ } catch (e: IOException) {
+ return null
+ }
+ }
+
+ /**
+ * Add image to cache.
+ * @param imageUrl url of image.
+ * @param response http response from page.
+ * @throws IOException image error.
+ */
+ @Throws(IOException::class)
+ fun putImageToCache(imageUrl: String, response: Response) {
+ // Initialize editor (edits the values for an entry).
+ var editor: DiskLruCache.Editor? = null
+
+ try {
+ // Get editor from md5 key.
+ val key = DiskUtils.hashKeyForDisk(imageUrl)
+ editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
+
+ // Get OutputStream and write image with Okio.
+ Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
+ it.writeAll(response.body().source())
+ it.flush()
+ }
+
+ diskCache.flush()
+ editor.commit()
+ } catch (e: Exception) {
+ response.body().close()
+ throw IOException("Unable to save image")
+ } finally {
+ editor?.abortUnlessCommitted()
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java
deleted file mode 100644
index 17ede8122..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.java
+++ /dev/null
@@ -1,235 +0,0 @@
-package eu.kanade.tachiyomi.data.cache;
-
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-import android.widget.ImageView;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.load.engine.DiskCacheStrategy;
-import com.bumptech.glide.load.model.GlideUrl;
-import com.bumptech.glide.load.model.LazyHeaders;
-import com.bumptech.glide.request.animation.GlideAnimation;
-import com.bumptech.glide.request.target.SimpleTarget;
-import com.bumptech.glide.signature.StringSignature;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import eu.kanade.tachiyomi.util.DiskUtils;
-
-/**
- * Class used to create cover cache
- * It is used to store the covers of the library.
- * Makes use of Glide (which can avoid repeating requests) to download covers.
- * Names of files are created with the md5 of the thumbnail URL
- */
-public class CoverCache {
-
- /**
- * Name of cache directory.
- */
- private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache";
-
- /**
- * Interface to global information about an application environment.
- */
- private final Context context;
-
- /**
- * Cache directory used for cache management.
- */
- private final File cacheDir;
-
- /**
- * Constructor of CoverCache.
- *
- * @param context application environment interface.
- */
- public CoverCache(Context context) {
- this.context = context;
-
- // Get cache directory from parameter.
- cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY);
-
- // Create cache directory.
- createCacheDir();
- }
-
- /**
- * Create cache directory if it doesn't exist
- *
- * @return true if cache dir is created otherwise false.
- */
- private boolean createCacheDir() {
- return !cacheDir.exists() && cacheDir.mkdirs();
- }
-
- /**
- * Download the cover with Glide and save the file in this cache.
- *
- * @param thumbnailUrl url of thumbnail.
- * @param headers headers included in Glide request.
- */
- public void save(String thumbnailUrl, LazyHeaders headers) {
- save(thumbnailUrl, headers, null);
- }
-
- /**
- * Download the cover with Glide and save the file.
- *
- * @param thumbnailUrl url of thumbnail.
- * @param headers headers included in Glide request.
- * @param imageView imageView where picture should be displayed.
- */
- private void save(String thumbnailUrl, LazyHeaders headers, @Nullable ImageView imageView) {
- // Check if url is empty.
- if (TextUtils.isEmpty(thumbnailUrl))
- return;
-
- // Download the cover with Glide and save the file.
- GlideUrl url = new GlideUrl(thumbnailUrl, headers);
- Glide.with(context)
- .load(url)
- .downloadOnly(new SimpleTarget() {
- @Override
- public void onResourceReady(File resource, GlideAnimation super File> anim) {
- try {
- // Copy the cover from Glide's cache to local cache.
- copyToLocalCache(thumbnailUrl, resource);
-
- // Check if imageView isn't null and show picture in imageView.
- if (imageView != null) {
- loadFromCache(imageView, resource);
- }
- } catch (IOException e) {
- // Do nothing.
- }
- }
- });
- }
-
- /**
- * Copy the cover from Glide's cache to this cache.
- *
- * @param thumbnailUrl url of thumbnail.
- * @param source the cover image.
- * @throws IOException exception returned
- */
- public void copyToLocalCache(String thumbnailUrl, File source) throws IOException {
- // Create cache directory if needed.
- createCacheDir();
-
- // Get destination file.
- File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
-
- // Delete the current file if it exists.
- if (dest.exists())
- dest.delete();
-
- // Write thumbnail image to file.
- InputStream in = new FileInputStream(source);
- try {
- OutputStream out = new FileOutputStream(dest);
- try {
- // Transfer bytes from in to out.
- byte[] buf = new byte[1024];
- int len;
- while ((len = in.read(buf)) > 0) {
- out.write(buf, 0, len);
- }
- } finally {
- out.close();
- }
- } finally {
- in.close();
- }
- }
-
-
- /**
- * Returns the cover from cache.
- *
- * @param thumbnailUrl the thumbnail url.
- * @return cover image.
- */
- private File getCoverFromCache(String thumbnailUrl) {
- return new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
- }
-
- /**
- * Delete the cover file from the cache.
- *
- * @param thumbnailUrl the thumbnail url.
- * @return status of deletion.
- */
- public boolean deleteCoverFromCache(String thumbnailUrl) {
- // Check if url is empty.
- if (TextUtils.isEmpty(thumbnailUrl))
- return false;
-
- // Remove file.
- File file = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
- return file.exists() && file.delete();
- }
-
- /**
- * Save or load the image from cache
- *
- * @param imageView imageView where picture should be displayed.
- * @param thumbnailUrl the thumbnail url.
- * @param headers headers included in Glide request.
- */
- public void saveOrLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
- // If file exist load it otherwise save it.
- File localCover = getCoverFromCache(thumbnailUrl);
- if (localCover.exists()) {
- loadFromCache(imageView, localCover);
- } else {
- save(thumbnailUrl, headers, imageView);
- }
- }
-
- /**
- * Helper method to load the cover from the cache directory into the specified image view.
- * Glide stores the resized image in its cache to improve performance.
- *
- * @param imageView imageView where picture should be displayed.
- * @param file file to load. Must exist!.
- */
- private void loadFromCache(ImageView imageView, File file) {
- Glide.with(context)
- .load(file)
- .diskCacheStrategy(DiskCacheStrategy.RESULT)
- .centerCrop()
- .signature(new StringSignature(String.valueOf(file.lastModified())))
- .into(imageView);
- }
-
- /**
- * Helper method to load the cover from network into the specified image view.
- * The source image is stored in Glide's cache so that it can be easily copied to this cache
- * if the manga is added to the library.
- *
- * @param imageView imageView where picture should be displayed.
- * @param thumbnailUrl url of thumbnail.
- * @param headers headers included in Glide request.
- */
- public void loadFromNetwork(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
- // Check if url is empty.
- if (TextUtils.isEmpty(thumbnailUrl))
- return;
-
- GlideUrl url = new GlideUrl(thumbnailUrl, headers);
- Glide.with(context)
- .load(url)
- .diskCacheStrategy(DiskCacheStrategy.SOURCE)
- .centerCrop()
- .into(imageView);
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
new file mode 100644
index 000000000..fb78a4f31
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
@@ -0,0 +1,158 @@
+package eu.kanade.tachiyomi.data.cache
+
+import android.content.Context
+import android.text.TextUtils
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.model.GlideUrl
+import com.bumptech.glide.load.model.LazyHeaders
+import com.bumptech.glide.request.animation.GlideAnimation
+import com.bumptech.glide.request.target.SimpleTarget
+import com.bumptech.glide.signature.StringSignature
+import eu.kanade.tachiyomi.util.DiskUtils
+import java.io.File
+import java.io.IOException
+
+/**
+ * Class used to create cover cache.
+ * It is used to store the covers of the library.
+ * Makes use of Glide (which can avoid repeating requests) to download covers.
+ * Names of files are created with the md5 of the thumbnail URL.
+ *
+ * @param context the application context.
+ * @constructor creates an instance of the cover cache.
+ */
+class CoverCache(private val context: Context) {
+
+ /**
+ * Cache directory used for cache management.
+ */
+ private val CACHE_DIRNAME = "cover_disk_cache"
+ private val cacheDir: File = File(context.cacheDir, CACHE_DIRNAME)
+
+ /**
+ * Download the cover with Glide and save the file.
+ * @param thumbnailUrl url of thumbnail.
+ * @param headers headers included in Glide request.
+ * @param imageView imageView where picture should be displayed.
+ */
+ @JvmOverloads
+ fun save(thumbnailUrl: String, headers: LazyHeaders, imageView: ImageView? = null) {
+ // Check if url is empty.
+ if (TextUtils.isEmpty(thumbnailUrl))
+ return
+
+ // Download the cover with Glide and save the file.
+ val url = GlideUrl(thumbnailUrl, headers)
+ Glide.with(context)
+ .load(url)
+ .downloadOnly(object : SimpleTarget() {
+ override fun onResourceReady(resource: File, anim: GlideAnimation) {
+ try {
+ // Copy the cover from Glide's cache to local cache.
+ copyToLocalCache(thumbnailUrl, resource)
+
+ // Check if imageView isn't null and show picture in imageView.
+ if (imageView != null) {
+ loadFromCache(imageView, resource)
+ }
+ } catch (e: IOException) {
+ // Do nothing.
+ }
+ }
+ })
+ }
+
+ /**
+ * Copy the cover from Glide's cache to this cache.
+ * @param thumbnailUrl url of thumbnail.
+ * @param sourceFile the source file of the cover image.
+ * @throws IOException exception returned
+ */
+ @Throws(IOException::class)
+ fun copyToLocalCache(thumbnailUrl: String, sourceFile: File) {
+ // Get destination file.
+ val destFile = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
+
+ sourceFile.copyTo(destFile, overwrite = true)
+ }
+
+
+ /**
+ * Returns the cover from cache.
+ * @param thumbnailUrl the thumbnail url.
+ * @return cover image.
+ */
+ private fun getCoverFromCache(thumbnailUrl: String): File {
+ return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
+ }
+
+ /**
+ * Delete the cover file from the cache.
+ * @param thumbnailUrl the thumbnail url.
+ * @return status of deletion.
+ */
+ fun deleteCoverFromCache(thumbnailUrl: String): Boolean {
+ // Check if url is empty.
+ if (TextUtils.isEmpty(thumbnailUrl))
+ return false
+
+ // Remove file.
+ val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
+ return file.exists() && file.delete()
+ }
+
+ /**
+ * Save or load the image from cache
+ * @param imageView imageView where picture should be displayed.
+ * @param thumbnailUrl the thumbnail url.
+ * @param headers headers included in Glide request.
+ */
+ fun saveOrLoadFromCache(imageView: ImageView, thumbnailUrl: String, headers: LazyHeaders) {
+ // If file exist load it otherwise save it.
+ val localCover = getCoverFromCache(thumbnailUrl)
+ if (localCover.exists()) {
+ loadFromCache(imageView, localCover)
+ } else {
+ save(thumbnailUrl, headers, imageView)
+ }
+ }
+
+ /**
+ * Helper method to load the cover from the cache directory into the specified image view.
+ * Glide stores the resized image in its cache to improve performance.
+ * @param imageView imageView where picture should be displayed.
+ * @param file file to load. Must exist!.
+ */
+ private fun loadFromCache(imageView: ImageView, file: File) {
+ Glide.with(context)
+ .load(file)
+ .diskCacheStrategy(DiskCacheStrategy.RESULT)
+ .centerCrop()
+ .signature(StringSignature(file.lastModified().toString()))
+ .into(imageView)
+ }
+
+ /**
+ * Helper method to load the cover from network into the specified image view.
+ * The source image is stored in Glide's cache so that it can be easily copied to this cache
+ * if the manga is added to the library.
+ * @param imageView imageView where picture should be displayed.
+ * @param thumbnailUrl url of thumbnail.
+ * @param headers headers included in Glide request.
+ */
+ fun loadFromNetwork(imageView: ImageView, thumbnailUrl: String, headers: LazyHeaders) {
+ // Check if url is empty.
+ if (TextUtils.isEmpty(thumbnailUrl))
+ return
+
+ val url = GlideUrl(thumbnailUrl, headers)
+ Glide.with(context)
+ .load(url)
+ .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+ .centerCrop()
+ .into(imageView)
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.java b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.java
deleted file mode 100644
index c02cf9ce7..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package eu.kanade.tachiyomi.data.cache;
-
-import android.content.Context;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.GlideBuilder;
-import com.bumptech.glide.load.DecodeFormat;
-import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
-import com.bumptech.glide.module.GlideModule;
-
-/**
- * Class used to update Glide module settings
- */
-public class CoverGlideModule implements GlideModule {
-
- @Override
- public void applyOptions(Context context, GlideBuilder builder) {
- // Bitmaps decoded from most image formats (other than GIFs with hidden configs)
- // will be decoded with the ARGB_8888 config.
- builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
-
- // Set the cache size of Glide to 15 MiB
- builder.setDiskCache(new InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024));
- }
-
- @Override
- public void registerComponents(Context context, Glide glide) {
- // Nothing to see here!
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
new file mode 100644
index 000000000..3e7504802
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
@@ -0,0 +1,22 @@
+package eu.kanade.tachiyomi.data.cache
+
+import android.content.Context
+import com.bumptech.glide.Glide
+import com.bumptech.glide.GlideBuilder
+import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
+import com.bumptech.glide.module.GlideModule
+
+/**
+ * Class used to update Glide module settings
+ */
+class CoverGlideModule : GlideModule {
+
+ override fun applyOptions(context: Context, builder: GlideBuilder) {
+ // Set the cache size of Glide to 15 MiB
+ builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
+ }
+
+ override fun registerComponents(context: Context, glide: Glide) {
+ // Nothing to see here!
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarm.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarm.kt
new file mode 100644
index 000000000..8f2368406
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarm.kt
@@ -0,0 +1,82 @@
+package eu.kanade.tachiyomi.data.library
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.SystemClock
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.util.alarmManager
+
+/**
+ * This class is used to update the library by firing an alarm after a specified time.
+ * It has a receiver reacting to system's boot and the intent fired by this alarm.
+ * See [onReceive] for more information.
+ */
+class LibraryUpdateAlarm : BroadcastReceiver() {
+
+ companion object {
+ const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY"
+
+ /**
+ * Sets the alarm to run the intent that updates the library.
+ * @param context the application context.
+ * @param intervalInHours the time in hours when it will be executed. Defaults to the
+ * value stored in preferences.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun startAlarm(context: Context,
+ intervalInHours: Int = PreferencesHelper.getLibraryUpdateInterval(context)) {
+ // Stop previous running alarms if needed, and do not restart it if the interval is 0.
+ stopAlarm(context)
+ if (intervalInHours == 0)
+ return
+
+ // Get the time the alarm should fire the event to update.
+ val intervalInMillis = intervalInHours * 60 * 60 * 1000
+ val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
+
+ // Start the alarm.
+ val pendingIntent = getPendingIntent(context)
+ context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ nextRun, intervalInMillis.toLong(), pendingIntent)
+ }
+
+ /**
+ * Stops the alarm if it's running.
+ * @param context the application context.
+ */
+ fun stopAlarm(context: Context) {
+ val pendingIntent = getPendingIntent(context)
+ context.alarmManager.cancel(pendingIntent)
+ }
+
+ /**
+ * Get the intent the alarm should run when it's fired.
+ * @param context the application context.
+ * @return the intent that will run when the alarm is fired.
+ */
+ private fun getPendingIntent(context: Context): PendingIntent {
+ val intent = Intent(context, LibraryUpdateAlarm::class.java)
+ intent.action = LIBRARY_UPDATE_ACTION
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+ }
+
+ /**
+ * Handle the intents received by this [BroadcastReceiver].
+ * @param context the application context.
+ * @param intent the intent to process.
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ // Start the alarm when the system is booted.
+ Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
+ // Update the library when the alarm fires an event.
+ LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context)
+ }
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
new file mode 100644
index 000000000..fa71ca68c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
@@ -0,0 +1,348 @@
+package eu.kanade.tachiyomi.data.library
+
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import android.os.PowerManager
+import android.support.v4.app.NotificationCompat
+import android.util.Pair
+import eu.kanade.tachiyomi.App
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.util.AndroidComponentUtil
+import eu.kanade.tachiyomi.util.NetworkUtil
+import eu.kanade.tachiyomi.util.notification
+import rx.Observable
+import rx.Subscription
+import rx.schedulers.Schedulers
+import timber.log.Timber
+import java.util.*
+import java.util.concurrent.atomic.AtomicInteger
+import javax.inject.Inject
+
+/**
+ * Get the start intent for [LibraryUpdateService].
+ * @param context the application context.
+ * @return the intent of the service.
+ */
+fun getStartIntent(context: Context): Intent {
+ return Intent(context, LibraryUpdateService::class.java)
+}
+
+/**
+ * Returns the status of the service.
+ * @param context the application context.
+ * @return true if the service is running, false otherwise.
+ */
+fun isRunning(context: Context): Boolean {
+ return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
+}
+
+/**
+ * This class will take care of updating the chapters of the manga from the library. It can be
+ * started calling the [start] method. If it's already running, it won't do anything.
+ * While the library is updating, a [PowerManager.WakeLock] will be held until the update is
+ * completed, preventing the device from going to sleep mode. A notification will display the
+ * progress of the update, and if case of an unexpected error, this service will be silently
+ * destroyed.
+ */
+class LibraryUpdateService : Service() {
+
+ // Dependencies injected through dagger.
+ @Inject lateinit var db: DatabaseHelper
+ @Inject lateinit var sourceManager: SourceManager
+ @Inject lateinit var preferences: PreferencesHelper
+
+ // Wake lock that will be held until the service is destroyed.
+ private lateinit var wakeLock: PowerManager.WakeLock
+
+ // Subscription where the update is done.
+ private var subscription: Subscription? = null
+
+ companion object {
+ val UPDATE_NOTIFICATION_ID = 1
+
+ /**
+ * Static method to start the service. It will be started only if there isn't another
+ * instance already running.
+ * @param context the application context.
+ */
+ @JvmStatic
+ fun start(context: Context) {
+ if (!isRunning(context)) {
+ context.startService(getStartIntent(context))
+ }
+ }
+
+ }
+
+ /**
+ * Method called when the service is created. It injects dagger dependencies and acquire
+ * the wake lock.
+ */
+ override fun onCreate() {
+ super.onCreate()
+ App.get(this).component.inject(this)
+ createAndAcquireWakeLock()
+ }
+
+ /**
+ * Method called when the service is destroyed. It destroy the running subscription, resets
+ * the alarm and release the wake lock.
+ */
+ override fun onDestroy() {
+ subscription?.unsubscribe()
+ LibraryUpdateAlarm.startAlarm(this)
+ destroyWakeLock()
+ super.onDestroy()
+ }
+
+ /**
+ * This method needs to be implemented, but it's not used/needed.
+ */
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ /**
+ * Method called when the service receives an intent. In this case, the content of the intent
+ * is irrelevant, because everything required is fetched in [updateLibrary].
+ * @param intent the intent from [start].
+ * @param flags the flags of the command.
+ * @param startId the start id of this command.
+ * @return the start value of the command.
+ */
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ // If there's no network available, set a component to start this service again when
+ // a connection is available.
+ if (!NetworkUtil.isNetworkConnected(this)) {
+ Timber.i("Sync canceled, connection not available")
+ AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
+ stopSelf(startId)
+ return Service.START_NOT_STICKY
+ }
+
+ // Unsubscribe from any previous subscription if needed.
+ subscription?.unsubscribe()
+
+ // Update favorite manga. Destroy service when completed or in case of an error.
+ subscription = Observable.defer { updateLibrary() }
+ .subscribeOn(Schedulers.io())
+ .subscribe({},
+ {
+ showNotification(getString(R.string.notification_update_error), "")
+ stopSelf(startId)
+ }, {
+ stopSelf(startId)
+ })
+
+ return Service.START_STICKY
+ }
+
+ /**
+ * Method that updates the library. It's called in a background thread, so it's safe to do
+ * heavy operations or network calls here.
+ * For each manga it calls [updateManga] and updates the notification showing the current
+ * progress.
+ * @return an observable delivering the progress of each update.
+ */
+ fun updateLibrary(): Observable {
+ // Initialize the variables holding the progress of the updates.
+ val count = AtomicInteger(0)
+ val newUpdates = ArrayList()
+ val failedUpdates = ArrayList()
+
+ // Get the manga list that is going to be updated.
+ val allLibraryMangas = db.favoriteMangas.executeAsBlocking()
+ val toUpdate = if (!preferences.updateOnlyNonCompleted())
+ allLibraryMangas
+ else
+ allLibraryMangas.filter { it.status != Manga.COMPLETED }
+
+ // Emit each manga and update it sequentially.
+ return Observable.from(toUpdate)
+ // Notify manga that will update.
+ .doOnNext { showProgressNotification(it, count.andIncrement, toUpdate.size) }
+ // Update the chapters of the manga.
+ .concatMap { manga -> updateManga(manga)
+ // If there's any error, return empty update and continue.
+ .onErrorReturn {
+ failedUpdates.add(manga)
+ Pair(0, 0)
+ }
+ // Filter out mangas without new chapters (or failed).
+ .filter { pair -> pair.first > 0 }
+ // Convert to the manga that contains new chapters.
+ .map { manga }
+ }
+ // Add manga with new chapters to the list.
+ .doOnNext { newUpdates.add(it) }
+ // Notify result of the overall update.
+ .doOnCompleted {
+ if (newUpdates.isEmpty()) {
+ cancelNotification()
+ } else {
+ showResultNotification(newUpdates, failedUpdates)
+ }
+ }
+ }
+
+ /**
+ * Updates the chapters for the given manga and adds them to the database.
+ * @param manga the manga to update.
+ * @return a pair of the inserted and removed chapters.
+ */
+ fun updateManga(manga: Manga): Observable> {
+ return sourceManager.get(manga.source)!!
+ .pullChaptersFromNetwork(manga.url)
+ .flatMap { db.insertOrRemoveChapters(manga, it) }
+ }
+
+ /**
+ * Returns the text that will be displayed in the notification when there are new chapters.
+ * @param updates a list of manga that contains new chapters.
+ * @param failedUpdates a list of manga that failed to update.
+ * @return the body of the notification to display.
+ */
+ private fun getUpdatedMangasBody(updates: List, failedUpdates: List): String {
+ return with(StringBuilder()) {
+ if (updates.isEmpty()) {
+ append(getString(R.string.notification_no_new_chapters))
+ append("\n")
+ } else {
+ append(getString(R.string.notification_new_chapters))
+ for (manga in updates) {
+ append("\n")
+ append(manga.title)
+ }
+ }
+ if (!failedUpdates.isEmpty()) {
+ append("\n\n")
+ append(getString(R.string.notification_manga_update_failed))
+ for (manga in failedUpdates) {
+ append("\n")
+ append(manga.title)
+ }
+ }
+ toString()
+ }
+ }
+
+ /**
+ * Creates and acquires a wake lock until the library is updated.
+ */
+ private fun createAndAcquireWakeLock() {
+ wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
+ wakeLock.acquire()
+ }
+
+ /**
+ * Releases the wake lock if it's held.
+ */
+ private fun destroyWakeLock() {
+ if (wakeLock.isHeld) {
+ wakeLock.release()
+ }
+ }
+
+ /**
+ * Shows the notification with the given title and body.
+ * @param title the title of the notification.
+ * @param body the body of the notification.
+ */
+ private fun showNotification(title: String, body: String) {
+ val n = notification() {
+ setSmallIcon(R.drawable.ic_action_refresh)
+ setContentTitle(title)
+ setContentText(body)
+ }
+ notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
+ }
+
+ /**
+ * Shows the notification containing the currently updating manga and the progress.
+ * @param manga the manga that's being updated.
+ * @param current the current progress.
+ * @param total the total progress.
+ */
+ private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
+ val n = notification() {
+ setSmallIcon(R.drawable.ic_action_refresh)
+ setContentTitle(manga.title)
+ setProgress(total, current, false)
+ setOngoing(true)
+ }
+ notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
+ }
+
+ /**
+ * Shows the notification containing the result of the update done by the service.
+ * @param updates a list of manga with new updates.
+ * @param failed a list of manga that failed to update.
+ */
+ private fun showResultNotification(updates: List, failed: List) {
+ val title = getString(R.string.notification_update_completed)
+ val body = getUpdatedMangasBody(updates, failed)
+
+ val n = notification() {
+ setSmallIcon(R.drawable.ic_action_refresh)
+ setContentTitle(title)
+ setStyle(NotificationCompat.BigTextStyle().bigText(body))
+ setContentIntent(notificationIntent)
+ setAutoCancel(true)
+ }
+ notificationManager.notify(UPDATE_NOTIFICATION_ID, n)
+ }
+
+ /**
+ * Cancels the notification.
+ */
+ private fun cancelNotification() {
+ notificationManager.cancel(UPDATE_NOTIFICATION_ID)
+ }
+
+ /**
+ * Property that returns the notification manager.
+ */
+ private val notificationManager : NotificationManager
+ get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ /**
+ * Property that returns an intent to open the main activity.
+ */
+ private val notificationIntent: PendingIntent
+ get() {
+ val intent = Intent(this, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ /**
+ * Class that triggers the library to update when a connection is available. It receives
+ * network changes.
+ */
+ class SyncOnConnectionAvailable : BroadcastReceiver() {
+
+ /**
+ * Method called when a network change occurs.
+ * @param context the application context.
+ * @param intent the intent received.
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ if (NetworkUtil.isNetworkConnected(context)) {
+ AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
+ context.startService(getStartIntent(context))
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.java b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.java
deleted file mode 100644
index 3411b852b..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package eu.kanade.tachiyomi.data.mangasync;
-
-import android.content.Context;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
-import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
-
-public class MangaSyncManager {
-
- private List services;
- private MyAnimeList myAnimeList;
-
- public static final int MYANIMELIST = 1;
-
- public MangaSyncManager(Context context) {
- services = new ArrayList<>();
- myAnimeList = new MyAnimeList(context);
- services.add(myAnimeList);
- }
-
- public MyAnimeList getMyAnimeList() {
- return myAnimeList;
- }
-
- public List getSyncServices() {
- return services;
- }
-
- public MangaSyncService getSyncService(int id) {
- switch (id) {
- case MYANIMELIST:
- return myAnimeList;
- }
- return null;
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt
new file mode 100644
index 000000000..54a29975a
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/MangaSyncManager.kt
@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.data.mangasync
+
+import android.content.Context
+import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
+import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList
+
+class MangaSyncManager(private val context: Context) {
+
+ val services: List
+ val myAnimeList: MyAnimeList
+
+ companion object {
+ const val MYANIMELIST = 1
+ }
+
+ init {
+ myAnimeList = MyAnimeList(context, MYANIMELIST)
+ services = listOf(myAnimeList)
+ }
+
+ fun getService(id: Int): MangaSyncService = services.find { it.id == id }!!
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt
new file mode 100644
index 000000000..81f0c7459
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/UpdateMangaSyncService.kt
@@ -0,0 +1,78 @@
+package eu.kanade.tachiyomi.data.mangasync
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import eu.kanade.tachiyomi.App
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.MangaSync
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import rx.subscriptions.CompositeSubscription
+import javax.inject.Inject
+
+class UpdateMangaSyncService : Service() {
+
+ @Inject lateinit var syncManager: MangaSyncManager
+ @Inject lateinit var db: DatabaseHelper
+
+ private lateinit var subscriptions: CompositeSubscription
+
+ override fun onCreate() {
+ super.onCreate()
+ App.get(this).component.inject(this)
+ subscriptions = CompositeSubscription()
+ }
+
+ override fun onDestroy() {
+ subscriptions.unsubscribe()
+ super.onDestroy()
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
+ if (manga != null) {
+ updateLastChapterRead(manga as MangaSync, startId)
+ return Service.START_REDELIVER_INTENT
+ } else {
+ stopSelf(startId)
+ return Service.START_NOT_STICKY
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
+ val sync = syncManager.getService(mangaSync.sync_id)
+
+ subscriptions.add(Observable.defer { sync.update(mangaSync) }
+ .flatMap {
+ if (it.isSuccessful) {
+ db.insertMangaSync(mangaSync).asRxObservable()
+ } else {
+ Observable.error(Exception("Could not update manga in remote service"))
+ }
+ }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ stopSelf(startId) },
+ { stopSelf(startId) }))
+ }
+
+ companion object {
+
+ private val EXTRA_MANGASYNC = "extra_mangasync"
+
+ @JvmStatic
+ fun start(context: Context, mangaSync: MangaSync) {
+ val intent = Intent(context, UpdateMangaSyncService::class.java)
+ intent.putExtra(EXTRA_MANGASYNC, mangaSync)
+ context.startService(intent)
+ }
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/base/MangaSyncService.java b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/base/MangaSyncService.java
deleted file mode 100644
index 1445447fc..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/base/MangaSyncService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package eu.kanade.tachiyomi.data.mangasync.base;
-
-import eu.kanade.tachiyomi.data.database.models.MangaSync;
-import okhttp3.Response;
-import rx.Observable;
-
-public abstract class MangaSyncService {
-
- // Name of the manga sync service to display
- public abstract String getName();
-
- // Id of the sync service (must be declared and obtained from MangaSyncManager to avoid conflicts)
- public abstract int getId();
-
- public abstract Observable login(String username, String password);
-
- public abstract boolean isLogged();
-
- public abstract Observable update(MangaSync manga);
-
- public abstract Observable add(MangaSync manga);
-
- public abstract Observable bind(MangaSync manga);
-
- public abstract String getStatus(int status);
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/base/MangaSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/base/MangaSyncService.kt
new file mode 100644
index 000000000..723abdc51
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/base/MangaSyncService.kt
@@ -0,0 +1,38 @@
+package eu.kanade.tachiyomi.data.mangasync.base
+
+import android.content.Context
+import eu.kanade.tachiyomi.App
+import eu.kanade.tachiyomi.data.database.models.MangaSync
+import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import okhttp3.Response
+import rx.Observable
+import javax.inject.Inject
+
+abstract class MangaSyncService(private val context: Context, val id: Int) {
+
+ @Inject lateinit var preferences: PreferencesHelper
+ @Inject lateinit var networkService: NetworkHelper
+
+ init {
+ App.get(context).component.inject(this)
+ }
+
+ // Name of the manga sync service to display
+ abstract val name: String
+
+ abstract fun login(username: String, password: String): Observable
+
+ open val isLogged: Boolean
+ get() = !preferences.getMangaSyncUsername(this).isEmpty() &&
+ !preferences.getMangaSyncPassword(this).isEmpty()
+
+ abstract fun update(manga: MangaSync): Observable
+
+ abstract fun add(manga: MangaSync): Observable
+
+ abstract fun bind(manga: MangaSync): Observable
+
+ abstract fun getStatus(status: Int): String
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.java b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.java
deleted file mode 100644
index add73acc9..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package eu.kanade.tachiyomi.data.mangasync.services;
-
-import android.content.Context;
-import android.net.Uri;
-import android.util.Xml;
-
-import org.jsoup.Jsoup;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.util.List;
-
-import javax.inject.Inject;
-
-import eu.kanade.tachiyomi.App;
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.database.models.MangaSync;
-import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
-import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
-import eu.kanade.tachiyomi.data.network.NetworkHelper;
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import okhttp3.Credentials;
-import okhttp3.FormBody;
-import okhttp3.Headers;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import rx.Observable;
-
-public class MyAnimeList extends MangaSyncService {
-
- @Inject PreferencesHelper preferences;
- @Inject NetworkHelper networkService;
-
- private Headers headers;
- private String username;
-
- public static final String BASE_URL = "http://myanimelist.net";
-
- private static final String ENTRY_TAG = "entry";
- private static final String CHAPTER_TAG = "chapter";
- private static final String SCORE_TAG = "score";
- private static final String STATUS_TAG = "status";
-
- public static final int READING = 1;
- public static final int COMPLETED = 2;
- public static final int ON_HOLD = 3;
- public static final int DROPPED = 4;
- public static final int PLAN_TO_READ = 6;
-
- public static final int DEFAULT_STATUS = READING;
- public static final int DEFAULT_SCORE = 0;
-
- private Context context;
-
- public MyAnimeList(Context context) {
- this.context = context;
- App.get(context).getComponent().inject(this);
-
- String username = preferences.getMangaSyncUsername(this);
- String password = preferences.getMangaSyncPassword(this);
-
- if (!username.isEmpty() && !password.isEmpty()) {
- createHeaders(username, password);
- }
- }
-
- @Override
- public String getName() {
- return "MyAnimeList";
- }
-
- @Override
- public int getId() {
- return MangaSyncManager.MYANIMELIST;
- }
-
- public String getLoginUrl() {
- return Uri.parse(BASE_URL).buildUpon()
- .appendEncodedPath("api/account/verify_credentials.xml")
- .toString();
- }
-
- public Observable login(String username, String password) {
- createHeaders(username, password);
- return networkService.getResponse(getLoginUrl(), headers, false)
- .map(response -> response.code() == 200);
- }
-
- @Override
- public boolean isLogged() {
- return !preferences.getMangaSyncUsername(this).isEmpty()
- && !preferences.getMangaSyncPassword(this).isEmpty();
- }
-
- public String getSearchUrl(String query) {
- return Uri.parse(BASE_URL).buildUpon()
- .appendEncodedPath("api/manga/search.xml")
- .appendQueryParameter("q", query)
- .toString();
- }
-
- public Observable> search(String query) {
- return networkService.getStringResponse(getSearchUrl(query), headers, true)
- .map(Jsoup::parse)
- .flatMap(doc -> Observable.from(doc.select("entry")))
- .filter(entry -> !entry.select("type").text().equals("Novel"))
- .map(entry -> {
- MangaSync manga = MangaSync.create(this);
- manga.title = entry.select("title").first().text();
- manga.remote_id = Integer.parseInt(entry.select("id").first().text());
- manga.total_chapters = Integer.parseInt(entry.select("chapters").first().text());
- return manga;
- })
- .toList();
- }
-
- public String getListUrl(String username) {
- return Uri.parse(BASE_URL).buildUpon()
- .appendPath("malappinfo.php")
- .appendQueryParameter("u", username)
- .appendQueryParameter("status", "all")
- .appendQueryParameter("type", "manga")
- .toString();
- }
-
- public Observable> getList() {
- // TODO cache this list for a few minutes
- return networkService.getStringResponse(getListUrl(username), headers, true)
- .map(Jsoup::parse)
- .flatMap(doc -> Observable.from(doc.select("manga")))
- .map(entry -> {
- MangaSync manga = MangaSync.create(this);
- manga.title = entry.select("series_title").first().text();
- manga.remote_id = Integer.parseInt(
- entry.select("series_mangadb_id").first().text());
- manga.last_chapter_read = Integer.parseInt(
- entry.select("my_read_chapters").first().text());
- manga.status = Integer.parseInt(
- entry.select("my_status").first().text());
- // MAL doesn't support score with decimals
- manga.score = Integer.parseInt(
- entry.select("my_score").first().text());
- manga.total_chapters = Integer.parseInt(
- entry.select("series_chapters").first().text());
- return manga;
- })
- .toList();
- }
-
- public String getUpdateUrl(MangaSync manga) {
- return Uri.parse(BASE_URL).buildUpon()
- .appendEncodedPath("api/mangalist/update")
- .appendPath(manga.remote_id + ".xml")
- .toString();
- }
-
- public Observable update(MangaSync manga) {
- try {
- if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
- manga.status = COMPLETED;
- }
- RequestBody payload = getMangaPostPayload(manga);
- return networkService.postData(getUpdateUrl(manga), payload, headers);
- } catch (IOException e) {
- return Observable.error(e);
- }
- }
-
- public String getAddUrl(MangaSync manga) {
- return Uri.parse(BASE_URL).buildUpon()
- .appendEncodedPath("api/mangalist/add")
- .appendPath(manga.remote_id + ".xml")
- .toString();
- }
-
- public Observable add(MangaSync manga) {
- try {
- RequestBody payload = getMangaPostPayload(manga);
- return networkService.postData(getAddUrl(manga), payload, headers);
- } catch (IOException e) {
- return Observable.error(e);
- }
- }
-
- private RequestBody getMangaPostPayload(MangaSync manga) throws IOException {
- XmlSerializer xml = Xml.newSerializer();
- StringWriter writer = new StringWriter();
- xml.setOutput(writer);
- xml.startDocument("UTF-8", false);
- xml.startTag("", ENTRY_TAG);
-
- // Last chapter read
- if (manga.last_chapter_read != 0) {
- xml.startTag("", CHAPTER_TAG);
- xml.text(manga.last_chapter_read + "");
- xml.endTag("", CHAPTER_TAG);
- }
- // Manga status in the list
- xml.startTag("", STATUS_TAG);
- xml.text(manga.status + "");
- xml.endTag("", STATUS_TAG);
- // Manga score
- xml.startTag("", SCORE_TAG);
- xml.text(manga.score + "");
- xml.endTag("", SCORE_TAG);
-
- xml.endTag("", ENTRY_TAG);
- xml.endDocument();
-
- FormBody.Builder form = new FormBody.Builder();
- form.add("data", writer.toString());
- return form.build();
- }
-
- public Observable bind(MangaSync manga) {
- return getList()
- .flatMap(list -> {
- manga.sync_id = getId();
- for (MangaSync remoteManga : list) {
- if (remoteManga.remote_id == manga.remote_id) {
- // Manga is already in the list
- manga.copyPersonalFrom(remoteManga);
- return update(manga);
- }
- }
- // Set default fields if it's not found in the list
- manga.score = DEFAULT_SCORE;
- manga.status = DEFAULT_STATUS;
- return add(manga);
- });
- }
-
- @Override
- public String getStatus(int status) {
- switch (status) {
- case READING:
- return context.getString(R.string.reading);
- case COMPLETED:
- return context.getString(R.string.completed);
- case ON_HOLD:
- return context.getString(R.string.on_hold);
- case DROPPED:
- return context.getString(R.string.dropped);
- case PLAN_TO_READ:
- return context.getString(R.string.plan_to_read);
- }
- return "";
- }
-
- public void createHeaders(String username, String password) {
- this.username = username;
- Headers.Builder builder = new Headers.Builder();
- builder.add("Authorization", Credentials.basic(username, password));
- builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C");
- setHeaders(builder.build());
- }
-
- public void setHeaders(Headers headers) {
- this.headers = headers;
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.kt
new file mode 100644
index 000000000..ab91b3050
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/mangasync/services/MyAnimeList.kt
@@ -0,0 +1,216 @@
+package eu.kanade.tachiyomi.data.mangasync.services
+
+import android.content.Context
+import android.net.Uri
+import android.util.Xml
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.MangaSync
+import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
+import eu.kanade.tachiyomi.data.network.get
+import eu.kanade.tachiyomi.data.network.post
+import eu.kanade.tachiyomi.util.selectInt
+import eu.kanade.tachiyomi.util.selectText
+import okhttp3.*
+import org.jsoup.Jsoup
+import org.xmlpull.v1.XmlSerializer
+import rx.Observable
+import java.io.StringWriter
+
+fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
+ startTag(namespace, tag)
+ text(body)
+ endTag(namespace, tag)
+}
+
+class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
+
+ private lateinit var headers: Headers
+ private lateinit var username: String
+
+ companion object {
+ val BASE_URL = "http://myanimelist.net"
+
+ private val ENTRY_TAG = "entry"
+ private val CHAPTER_TAG = "chapter"
+ private val SCORE_TAG = "score"
+ private val STATUS_TAG = "status"
+
+ val READING = 1
+ val COMPLETED = 2
+ val ON_HOLD = 3
+ val DROPPED = 4
+ val PLAN_TO_READ = 6
+
+ val DEFAULT_STATUS = READING
+ val DEFAULT_SCORE = 0
+ }
+
+ init {
+ val username = preferences.getMangaSyncUsername(this)
+ val password = preferences.getMangaSyncPassword(this)
+
+ if (!username.isEmpty() && !password.isEmpty()) {
+ createHeaders(username, password)
+ }
+ }
+
+ override val name: String
+ get() = "MyAnimeList"
+
+ fun getLoginUrl(): String {
+ return Uri.parse(BASE_URL).buildUpon()
+ .appendEncodedPath("api/account/verify_credentials.xml")
+ .toString()
+ }
+
+ override fun login(username: String, password: String): Observable {
+ createHeaders(username, password)
+ return networkService.request(get(getLoginUrl(), headers))
+ .map { it.code() == 200 }
+ }
+
+ fun getSearchUrl(query: String): String {
+ return Uri.parse(BASE_URL).buildUpon()
+ .appendEncodedPath("api/manga/search.xml")
+ .appendQueryParameter("q", query)
+ .toString()
+ }
+
+ fun search(query: String): Observable> {
+ return networkService.requestBody(get(getSearchUrl(query), headers))
+ .map { Jsoup.parse(it) }
+ .flatMap { Observable.from(it.select("entry")) }
+ .filter { it.select("type").text() != "Novel" }
+ .map {
+ val manga = MangaSync.create(this)
+ manga.title = it.selectText("title")
+ manga.remote_id = it.selectInt("id")
+ manga.total_chapters = it.selectInt("chapters")
+ manga
+ }
+ .toList()
+ }
+
+ fun getListUrl(username: String): String {
+ return Uri.parse(BASE_URL).buildUpon()
+ .appendPath("malappinfo.php")
+ .appendQueryParameter("u", username)
+ .appendQueryParameter("status", "all")
+ .appendQueryParameter("type", "manga")
+ .toString()
+ }
+
+ // MAL doesn't support score with decimals
+ fun getList(): Observable> {
+ return networkService.requestBody(get(getListUrl(username), headers), true)
+ .map { Jsoup.parse(it) }
+ .flatMap { Observable.from(it.select("manga")) }
+ .map {
+ val manga = MangaSync.create(this)
+ manga.title = it.selectText("series_title")
+ manga.remote_id = it.selectInt("series_mangadb_id")
+ manga.last_chapter_read = it.selectInt("my_read_chapters")
+ manga.status = it.selectInt("my_status")
+ manga.score = it.selectInt("my_score").toFloat()
+ manga.total_chapters = it.selectInt("series_chapters")
+ manga
+ }
+ .toList()
+ }
+
+ fun getUpdateUrl(manga: MangaSync): String {
+ return Uri.parse(BASE_URL).buildUpon()
+ .appendEncodedPath("api/mangalist/update")
+ .appendPath(manga.remote_id.toString() + ".xml")
+ .toString()
+ }
+
+ override fun update(manga: MangaSync): Observable {
+ return Observable.defer {
+ if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
+ manga.status = COMPLETED
+ }
+ networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
+ }
+
+ }
+
+ fun getAddUrl(manga: MangaSync): String {
+ return Uri.parse(BASE_URL).buildUpon()
+ .appendEncodedPath("api/mangalist/add")
+ .appendPath(manga.remote_id.toString() + ".xml")
+ .toString()
+ }
+
+ override fun add(manga: MangaSync): Observable {
+ return Observable.defer {
+ networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
+ }
+ }
+
+ private fun getMangaPostPayload(manga: MangaSync): RequestBody {
+ val xml = Xml.newSerializer()
+ val writer = StringWriter()
+
+ with(xml) {
+ setOutput(writer)
+ startDocument("UTF-8", false)
+ startTag("", ENTRY_TAG)
+
+ // Last chapter read
+ if (manga.last_chapter_read != 0) {
+ inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
+ }
+ // Manga status in the list
+ inTag(STATUS_TAG, manga.status.toString())
+
+ // Manga score
+ inTag(SCORE_TAG, manga.score.toString())
+
+ endTag("", ENTRY_TAG)
+ endDocument()
+ }
+
+ val form = FormBody.Builder()
+ form.add("data", writer.toString())
+ return form.build()
+ }
+
+ override fun bind(manga: MangaSync): Observable {
+ return getList()
+ .flatMap {
+ manga.sync_id = id
+ for (remoteManga in it) {
+ if (remoteManga.remote_id == manga.remote_id) {
+ // Manga is already in the list
+ manga.copyPersonalFrom(remoteManga)
+ return@flatMap update(manga)
+ }
+ }
+ // Set default fields if it's not found in the list
+ manga.score = DEFAULT_SCORE.toFloat()
+ manga.status = DEFAULT_STATUS
+ return@flatMap add(manga)
+ }
+ }
+
+ override fun getStatus(status: Int): String = with(context) {
+ when (status) {
+ READING -> getString(R.string.reading)
+ COMPLETED -> getString(R.string.completed)
+ ON_HOLD -> getString(R.string.on_hold)
+ DROPPED -> getString(R.string.dropped)
+ PLAN_TO_READ -> getString(R.string.plan_to_read)
+ else -> ""
+ }
+ }
+
+ fun createHeaders(username: String, password: String) {
+ this.username = username
+ val builder = Headers.Builder()
+ builder.add("Authorization", Credentials.basic(username, password))
+ builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
+ headers = builder.build()
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.java b/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.java
deleted file mode 100644
index 4ecbbaae6..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package eu.kanade.tachiyomi.data.network;
-
-
-import android.content.Context;
-
-import java.io.File;
-import java.net.CookieManager;
-import java.net.CookiePolicy;
-import java.net.CookieStore;
-import java.util.concurrent.TimeUnit;
-
-import okhttp3.Cache;
-import okhttp3.CacheControl;
-import okhttp3.FormBody;
-import okhttp3.Headers;
-import okhttp3.Interceptor;
-import okhttp3.JavaNetCookieJar;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import rx.Observable;
-
-public final class NetworkHelper {
-
- private OkHttpClient client;
- private OkHttpClient forceCacheClient;
-
- private CookieManager cookieManager;
-
- public final Headers NULL_HEADERS = new Headers.Builder().build();
- public final RequestBody NULL_REQUEST_BODY = new FormBody.Builder().build();
- public final CacheControl CACHE_CONTROL = new CacheControl.Builder()
- .maxAge(10, TimeUnit.MINUTES)
- .build();
-
- private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> {
- Response originalResponse = chain.proceed(chain.request());
- return originalResponse.newBuilder()
- .removeHeader("Pragma")
- .header("Cache-Control", "max-age=" + 600)
- .build();
- };
-
- private static final int CACHE_SIZE = 5 * 1024 * 1024; // 5 MiB
- private static final String CACHE_DIR_NAME = "network_cache";
-
- public NetworkHelper(Context context) {
- File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
-
- cookieManager = new CookieManager();
- cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
-
- client = new OkHttpClient.Builder()
- .cookieJar(new JavaNetCookieJar(cookieManager))
- .cache(new Cache(cacheDir, CACHE_SIZE))
- .build();
-
- forceCacheClient = client.newBuilder()
- .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
- .build();
- }
-
- public Observable getResponse(final String url, final Headers headers, boolean forceCache) {
- return Observable.defer(() -> {
- try {
- OkHttpClient c = forceCache ? forceCacheClient : client;
-
- Request request = new Request.Builder()
- .url(url)
- .headers(headers != null ? headers : NULL_HEADERS)
- .cacheControl(CACHE_CONTROL)
- .build();
-
- return Observable.just(c.newCall(request).execute());
- } catch (Throwable e) {
- return Observable.error(e);
- }
- }).retry(1);
- }
-
- public Observable mapResponseToString(final Response response) {
- return Observable.defer(() -> {
- try {
- return Observable.just(response.body().string());
- } catch (Throwable e) {
- return Observable.error(e);
- }
- });
- }
-
- public Observable getStringResponse(final String url, final Headers headers, boolean forceCache) {
- return getResponse(url, headers, forceCache)
- .flatMap(this::mapResponseToString);
- }
-
- public Observable postData(final String url, final RequestBody formBody, final Headers headers) {
- return Observable.defer(() -> {
- try {
- Request request = new Request.Builder()
- .url(url)
- .post(formBody != null ? formBody : NULL_REQUEST_BODY)
- .headers(headers != null ? headers : NULL_HEADERS)
- .build();
- return Observable.just(client.newCall(request).execute());
- } catch (Throwable e) {
- return Observable.error(e);
- }
- }).retry(1);
- }
-
- public Observable getProgressResponse(final String url, final Headers headers, final ProgressListener listener) {
- return Observable.defer(() -> {
- try {
- Request request = new Request.Builder()
- .url(url)
- .cacheControl(CacheControl.FORCE_NETWORK)
- .headers(headers != null ? headers : NULL_HEADERS)
- .build();
-
- OkHttpClient progressClient = client.newBuilder()
- .cache(null)
- .addNetworkInterceptor(chain -> {
- Response originalResponse = chain.proceed(chain.request());
- return originalResponse.newBuilder()
- .body(new ProgressResponseBody(originalResponse.body(), listener))
- .build();
- }).build();
-
- return Observable.just(progressClient.newCall(request).execute());
- } catch (Throwable e) {
- return Observable.error(e);
- }
- }).retry(1);
- }
-
- public CookieStore getCookies() {
- return cookieManager.getCookieStore();
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt
new file mode 100644
index 000000000..05225aad5
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt
@@ -0,0 +1,78 @@
+package eu.kanade.tachiyomi.data.network
+
+import android.content.Context
+import okhttp3.*
+import rx.Observable
+import java.io.File
+import java.net.CookieManager
+import java.net.CookiePolicy
+import java.net.CookieStore
+
+class NetworkHelper(context: Context) {
+
+ private val client: OkHttpClient
+ private val forceCacheClient: OkHttpClient
+
+ private val cookieManager: CookieManager
+
+ private val forceCacheInterceptor = { chain: Interceptor.Chain ->
+ val originalResponse = chain.proceed(chain.request())
+ originalResponse.newBuilder()
+ .removeHeader("Pragma")
+ .header("Cache-Control", "max-age=" + 600)
+ .build()
+ }
+
+ private val cacheSize = 5L * 1024 * 1024 // 5 MiB
+ private val cacheDir = "network_cache"
+
+ init {
+ val cacheDir = File(context.cacheDir, cacheDir)
+
+ cookieManager = CookieManager()
+ cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
+
+ client = OkHttpClient.Builder()
+ .cookieJar(JavaNetCookieJar(cookieManager))
+ .cache(Cache(cacheDir, cacheSize))
+ .build()
+
+ forceCacheClient = client.newBuilder()
+ .addNetworkInterceptor(forceCacheInterceptor)
+ .build()
+ }
+
+ @JvmOverloads
+ fun request(request: Request, forceCache: Boolean = false): Observable {
+ return Observable.fromCallable {
+ val c = if (forceCache) forceCacheClient else client
+ c.newCall(request).execute()
+ }
+ }
+
+ @JvmOverloads
+ fun requestBody(request: Request, forceCache: Boolean = false): Observable {
+ return request(request, forceCache)
+ .map { it.body().string() }
+ }
+
+ fun requestBodyProgress(request: Request, listener: ProgressListener): Observable {
+ return Observable.fromCallable {
+ val progressClient = client.newBuilder()
+ .cache(null)
+ .addNetworkInterceptor { chain ->
+ val originalResponse = chain.proceed(chain.request())
+ originalResponse.newBuilder()
+ .body(ProgressResponseBody(originalResponse.body(), listener))
+ .build()
+ }
+ .build()
+
+ progressClient.newCall(request).execute()
+ }.retry(1)
+ }
+
+ val cookies: CookieStore
+ get() = cookieManager.cookieStore
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.java b/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.java
deleted file mode 100644
index ae43b27f7..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package eu.kanade.tachiyomi.data.network;
-
-public interface ProgressListener {
- void update(long bytesRead, long contentLength, boolean done);
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.kt
new file mode 100644
index 000000000..f624e2b62
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.kt
@@ -0,0 +1,5 @@
+package eu.kanade.tachiyomi.data.network
+
+interface ProgressListener {
+ fun update(bytesRead: Long, contentLength: Long, done: Boolean)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.java b/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.java
deleted file mode 100644
index b74016bc4..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package eu.kanade.tachiyomi.data.network;
-
-import java.io.IOException;
-
-import okhttp3.MediaType;
-import okhttp3.ResponseBody;
-import okio.Buffer;
-import okio.BufferedSource;
-import okio.ForwardingSource;
-import okio.Okio;
-import okio.Source;
-
-public class ProgressResponseBody extends ResponseBody {
-
- private final ResponseBody responseBody;
- private final ProgressListener progressListener;
- private BufferedSource bufferedSource;
-
- public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
- this.responseBody = responseBody;
- this.progressListener = progressListener;
- }
-
- @Override public MediaType contentType() {
- return responseBody.contentType();
- }
-
- @Override public long contentLength() {
- return responseBody.contentLength();
- }
-
- @Override public BufferedSource source() {
- if (bufferedSource == null) {
- bufferedSource = Okio.buffer(source(responseBody.source()));
- }
- return bufferedSource;
- }
-
- private Source source(Source source) {
- return new ForwardingSource(source) {
- long totalBytesRead = 0L;
-
- @Override public long read(Buffer sink, long byteCount) throws IOException {
- long bytesRead = super.read(sink, byteCount);
- // read() returns the number of bytes read, or -1 if this source is exhausted.
- totalBytesRead += bytesRead != -1 ? bytesRead : 0;
- progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
- return bytesRead;
- }
- };
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.kt
new file mode 100644
index 000000000..67c639b1a
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.kt
@@ -0,0 +1,40 @@
+package eu.kanade.tachiyomi.data.network
+
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import okio.*
+import java.io.IOException
+
+class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
+
+ private val bufferedSource: BufferedSource by lazy {
+ Okio.buffer(source(responseBody.source()))
+ }
+
+ override fun contentType(): MediaType {
+ return responseBody.contentType()
+ }
+
+ override fun contentLength(): Long {
+ return responseBody.contentLength()
+ }
+
+ override fun source(): BufferedSource {
+ return bufferedSource
+ }
+
+ private fun source(source: Source): Source {
+ return object : ForwardingSource(source) {
+ internal var totalBytesRead = 0L
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ val bytesRead = super.read(sink, byteCount)
+ // read() returns the number of bytes read, or -1 if this source is exhausted.
+ totalBytesRead += if (bytesRead != -1L) bytesRead else 0
+ progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
+ return bytesRead
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/network/Req.kt b/app/src/main/java/eu/kanade/tachiyomi/data/network/Req.kt
new file mode 100644
index 000000000..0cedb2e97
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/network/Req.kt
@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.data.network
+
+import okhttp3.*
+import java.util.concurrent.TimeUnit
+
+private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
+private val DEFAULT_HEADERS = Headers.Builder().build()
+private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
+
+@JvmOverloads
+fun get(url: String,
+ headers: Headers = DEFAULT_HEADERS,
+ cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
+
+ return Request.Builder()
+ .url(url)
+ .headers(headers)
+ .cacheControl(cache)
+ .build()
+}
+
+@JvmOverloads
+fun post(url: String,
+ headers: Headers = DEFAULT_HEADERS,
+ body: RequestBody = DEFAULT_BODY,
+ cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
+
+ return Request.Builder()
+ .url(url)
+ .post(body)
+ .headers(headers)
+ .cacheControl(cache)
+ .build()
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.java b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.java
index 4e93f1768..342481f1f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.java
@@ -190,4 +190,8 @@ public class PreferencesHelper {
context.getString(R.string.pref_library_update_interval_key), 0);
}
+ public Preference libraryUpdateInterval() {
+ return rxPrefs.getInteger(getKey(R.string.pref_library_update_interval_key), 0);
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/rest/GithubService.java b/app/src/main/java/eu/kanade/tachiyomi/data/rest/GithubService.java
deleted file mode 100644
index 2f34f6b5b..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/rest/GithubService.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package eu.kanade.tachiyomi.data.rest;
-
-import retrofit.http.GET;
-import rx.Observable;
-
-
-/**
- * Used to connect with the Github API
- */
-public interface GithubService {
- String SERVICE_ENDPOINT = "https://api.github.com";
-
- @GET("/repos/inorichi/tachiyomi/releases/latest") Observable getLatestVersion();
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/rest/Release.java b/app/src/main/java/eu/kanade/tachiyomi/data/rest/Release.java
deleted file mode 100644
index b0f2398cb..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/rest/Release.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package eu.kanade.tachiyomi.data.rest;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.util.List;
-
-/**
- * Release object
- * Contains information about the latest release
- */
-public class Release {
- /**
- * Version name V0.0.0
- */
- @SerializedName("tag_name")
- private final String version;
-
- /** Change Log */
- @SerializedName("body")
- private final String log;
-
- /** Assets containing download url */
- @SerializedName("assets")
- private final List assets;
-
- /**
- * Release constructor
- *
- * @param version version of latest release
- * @param log log of latest release
- * @param assets assets of latest release
- */
- public Release(String version, String log, List assets) {
- this.version = version;
- this.log = log;
- this.assets = assets;
- }
-
- /**
- * Get latest release version
- *
- * @return latest release version
- */
- public String getVersion() {
- return version;
- }
-
- /**
- * Get change log of latest release
- *
- * @return change log of latest release
- */
- public String getChangeLog() {
- return log;
- }
-
- /**
- * Get download link of latest release
- *
- * @return download link of latest release
- */
- public String getDownloadLink() {
- return assets.get(0).getDownloadLink();
- }
-
- /**
- * Assets class containing download url
- */
- class Assets {
- @SerializedName("browser_download_url")
- private final String download_url;
-
-
- /**
- * Assets Constructor
- *
- * @param download_url download url
- */
- @SuppressWarnings("unused") public Assets(String download_url) {
- this.download_url = download_url;
- }
-
- /**
- * Get download link of latest release
- *
- * @return download link of latest release
- */
- public String getDownloadLink() {
- return download_url;
- }
- }
-}
-
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/rest/ServiceFactory.java b/app/src/main/java/eu/kanade/tachiyomi/data/rest/ServiceFactory.java
deleted file mode 100644
index 6cf455fda..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/rest/ServiceFactory.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package eu.kanade.tachiyomi.data.rest;
-
-import retrofit.RestAdapter;
-
-public class ServiceFactory {
-
- /**
- * Creates a retrofit service from an arbitrary class (clazz)
- *
- * @param clazz Java interface of the retrofit service
- * @param endPoint REST endpoint url
- * @return retrofit service with defined endpoint
- */
- public static T createRetrofitService(final Class clazz, final String endPoint) {
- final RestAdapter restAdapter = new RestAdapter.Builder()
- .setEndpoint(endPoint)
- .build();
-
- return restAdapter.create(clazz);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.java
deleted file mode 100644
index 2a7d183f1..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package eu.kanade.tachiyomi.data.source;
-
-import android.content.Context;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-
-import eu.kanade.tachiyomi.data.source.base.Source;
-import eu.kanade.tachiyomi.data.source.online.english.Batoto;
-import eu.kanade.tachiyomi.data.source.online.english.Kissmanga;
-import eu.kanade.tachiyomi.data.source.online.english.Mangafox;
-import eu.kanade.tachiyomi.data.source.online.english.Mangahere;
-
-public class SourceManager {
-
- public static final int BATOTO = 1;
- public static final int MANGAHERE = 2;
- public static final int MANGAFOX = 3;
- public static final int KISSMANGA = 4;
-
- private HashMap sourcesMap;
- private Context context;
-
- public SourceManager(Context context) {
- sourcesMap = new HashMap<>();
- this.context = context;
-
- initializeSources();
- }
-
- public Source get(int sourceKey) {
- if (!sourcesMap.containsKey(sourceKey)) {
- sourcesMap.put(sourceKey, createSource(sourceKey));
- }
- return sourcesMap.get(sourceKey);
- }
-
- private Source createSource(int sourceKey) {
- switch (sourceKey) {
- case BATOTO:
- return new Batoto(context);
- case MANGAHERE:
- return new Mangahere(context);
- case MANGAFOX:
- return new Mangafox(context);
- case KISSMANGA:
- return new Kissmanga(context);
- }
-
- return null;
- }
-
- private void initializeSources() {
- sourcesMap.put(BATOTO, createSource(BATOTO));
- sourcesMap.put(MANGAHERE, createSource(MANGAHERE));
- sourcesMap.put(MANGAFOX, createSource(MANGAFOX));
- sourcesMap.put(KISSMANGA, createSource(KISSMANGA));
- }
-
- public List getSources() {
- List sources = new ArrayList<>(sourcesMap.values());
- Collections.sort(sources, (s1, s2) -> s1.getName().compareTo(s2.getName()));
- return sources;
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt
new file mode 100644
index 000000000..f002e3ef7
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt
@@ -0,0 +1,52 @@
+package eu.kanade.tachiyomi.data.source
+
+import android.content.Context
+import eu.kanade.tachiyomi.data.source.base.Source
+import eu.kanade.tachiyomi.data.source.online.english.Batoto
+import eu.kanade.tachiyomi.data.source.online.english.Kissmanga
+import eu.kanade.tachiyomi.data.source.online.english.Mangafox
+import eu.kanade.tachiyomi.data.source.online.english.Mangahere
+import java.util.*
+
+open class SourceManager(private val context: Context) {
+
+ val sourcesMap: HashMap
+ val sources: List
+
+ val BATOTO = 1
+ val MANGAHERE = 2
+ val MANGAFOX = 3
+ val KISSMANGA = 4
+
+ val LAST_SOURCE = 4
+
+ init {
+ sourcesMap = createSourcesMap()
+ sources = ArrayList(sourcesMap.values).sortedBy { it.name }
+ }
+
+ open fun get(sourceKey: Int): Source? {
+ return sourcesMap[sourceKey]
+ }
+
+ private fun createSource(sourceKey: Int): Source? = when (sourceKey) {
+ BATOTO -> Batoto(context)
+ MANGAHERE -> Mangahere(context)
+ MANGAFOX -> Mangafox(context)
+ KISSMANGA -> Kissmanga(context)
+ else -> null
+ }
+
+ private fun createSourcesMap(): HashMap {
+ val map = HashMap()
+ for (i in 1..LAST_SOURCE) {
+ val source = createSource(i)
+ if (source != null) {
+ source.id = i
+ map.put(i, source)
+ }
+ }
+ return map
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/base/BaseSource.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/base/BaseSource.java
index 890c5b986..3410ae041 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/base/BaseSource.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/base/BaseSource.java
@@ -13,12 +13,20 @@ import rx.Observable;
public abstract class BaseSource {
+ private int id;
+
+ // Id of the source
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
// Name of the source to display
public abstract String getName();
- // Id of the source (must be declared and obtained from SourceManager to avoid conflicts)
- public abstract int getId();
-
// Base url of the source, like: http://example.com
public abstract String getBaseUrl();
@@ -68,24 +76,6 @@ public abstract class BaseSource {
protected boolean isAuthenticationSuccessful(Response response) {
throw new UnsupportedOperationException("Not implemented");
}
-
-
- // Default fields, they can be overriden by sources' implementation
-
- // Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls
- protected String overrideMangaUrl(String defaultMangaUrl) {
- return defaultMangaUrl;
- }
-
- // Get the URL of the first page that contains a source image and the page list
- protected String overrideChapterUrl(String defaultPageUrl) {
- return defaultPageUrl;
- }
-
- // Get the URL of the pages that contains source images
- protected String overridePageUrl(String defaultPageUrl) {
- return defaultPageUrl;
- }
// Default headers, it can be overriden by children or just add new keys
protected Headers.Builder headersBuilder() {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.java
index 417d60bd1..89e2738aa 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.java
@@ -18,10 +18,12 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.network.NetworkHelper;
+import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import okhttp3.Headers;
+import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.schedulers.Schedulers;
@@ -47,13 +49,46 @@ public abstract class Source extends BaseSource {
return false;
}
+ protected Request popularMangaRequest(MangasPage page) {
+ if (page.page == 1) {
+ page.url = getInitialPopularMangasUrl();
+ }
+
+ return ReqKt.get(page.url, requestHeaders);
+ }
+
+ protected Request searchMangaRequest(MangasPage page, String query) {
+ if (page.page == 1) {
+ page.url = getInitialSearchUrl(query);
+ }
+
+ return ReqKt.get(page.url, requestHeaders);
+ }
+
+ protected Request mangaDetailsRequest(String mangaUrl) {
+ return ReqKt.get(getBaseUrl() + mangaUrl, requestHeaders);
+ }
+
+ protected Request chapterListRequest(String mangaUrl) {
+ return ReqKt.get(getBaseUrl() + mangaUrl, requestHeaders);
+ }
+
+ protected Request pageListRequest(String chapterUrl) {
+ return ReqKt.get(getBaseUrl() + chapterUrl, requestHeaders);
+ }
+
+ protected Request imageUrlRequest(Page page) {
+ return ReqKt.get(page.getUrl(), requestHeaders);
+ }
+
+ protected Request imageRequest(Page page) {
+ return ReqKt.get(page.getImageUrl(), requestHeaders);
+ }
+
// Get the most popular mangas from the source
public Observable pullPopularMangasFromNetwork(MangasPage page) {
- if (page.page == 1)
- page.url = getInitialPopularMangasUrl();
-
return networkService
- .getStringResponse(page.url, requestHeaders, true)
+ .requestBody(popularMangaRequest(page), true)
.map(Jsoup::parse)
.doOnNext(doc -> page.mangas = parsePopularMangasFromHtml(doc))
.doOnNext(doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page))
@@ -62,11 +97,8 @@ public abstract class Source extends BaseSource {
// Get mangas from the source with a query
public Observable searchMangasFromNetwork(MangasPage page, String query) {
- if (page.page == 1)
- page.url = getInitialSearchUrl(query);
-
return networkService
- .getStringResponse(page.url, requestHeaders, true)
+ .requestBody(searchMangaRequest(page, query), true)
.map(Jsoup::parse)
.doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
.doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
@@ -76,14 +108,14 @@ public abstract class Source extends BaseSource {
// Get manga details from the source
public Observable pullMangaFromNetwork(final String mangaUrl) {
return networkService
- .getStringResponse(getBaseUrl() + overrideMangaUrl(mangaUrl), requestHeaders, true)
+ .requestBody(mangaDetailsRequest(mangaUrl))
.flatMap(unparsedHtml -> Observable.just(parseHtmlToManga(mangaUrl, unparsedHtml)));
}
// Get chapter list of a manga from the source
public Observable> pullChaptersFromNetwork(final String mangaUrl) {
return networkService
- .getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, false)
+ .requestBody(chapterListRequest(mangaUrl))
.flatMap(unparsedHtml -> {
List chapters = parseHtmlToChapters(unparsedHtml);
return !chapters.isEmpty() ?
@@ -102,7 +134,7 @@ public abstract class Source extends BaseSource {
public Observable> pullPageListFromNetwork(final String chapterUrl) {
return networkService
- .getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, false)
+ .requestBody(pageListRequest(chapterUrl))
.flatMap(unparsedHtml -> {
List pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
return !pages.isEmpty() ?
@@ -127,7 +159,7 @@ public abstract class Source extends BaseSource {
public Observable getImageUrlFromPage(final Page page) {
page.setStatus(Page.LOAD_PAGE);
return networkService
- .getStringResponse(overridePageUrl(page.getUrl()), requestHeaders, false)
+ .requestBody(imageUrlRequest(page))
.flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)))
.onErrorResumeNext(e -> {
page.setStatus(Page.ERROR);
@@ -177,7 +209,7 @@ public abstract class Source extends BaseSource {
}
public Observable getImageProgressResponse(final Page page) {
- return networkService.getProgressResponse(page.getImageUrl(), requestHeaders, page);
+ return networkService.requestBodyProgress(imageRequest(page), page);
}
public void savePageList(String chapterUrl, List pages) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java
index 827cab089..174a19006 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.java
@@ -27,13 +27,14 @@ import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.source.SourceManager;
+import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.base.LoginSource;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
+import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
@@ -41,11 +42,11 @@ public class Batoto extends LoginSource {
public static final String NAME = "Batoto (EN)";
public static final String BASE_URL = "http://bato.to";
- public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%d";
+ public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%s";
public static final String SEARCH_URL = BASE_URL + "/search_ajax?name=%s&p=%s";
- public static final String CHAPTER_URL = "/areader?id=%s&p=1";
+ public static final String CHAPTER_URL = BASE_URL + "/areader?id=%s&p=1";
public static final String PAGE_URL = BASE_URL + "/areader?id=%s&p=%s";
- public static final String MANGA_URL = "/comic_pop?id=%s";
+ public static final String MANGA_URL = BASE_URL + "/comic_pop?id=%s";
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global§ion=login";
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
@@ -73,11 +74,6 @@ public class Batoto extends LoginSource {
return NAME;
}
- @Override
- public int getId() {
- return SourceManager.BATOTO;
- }
-
@Override
public String getBaseUrl() {
return BASE_URL;
@@ -102,23 +98,24 @@ public class Batoto extends LoginSource {
}
@Override
- protected String overrideMangaUrl(String defaultMangaUrl) {
- String mangaId = defaultMangaUrl.substring(defaultMangaUrl.lastIndexOf("r") + 1);
- return String.format(MANGA_URL, mangaId);
+ protected Request mangaDetailsRequest(String mangaUrl) {
+ String mangaId = mangaUrl.substring(mangaUrl.lastIndexOf("r") + 1);
+ return ReqKt.get(String.format(MANGA_URL, mangaId), requestHeaders);
}
@Override
- protected String overrideChapterUrl(String defaultPageUrl) {
- String id = defaultPageUrl.substring(defaultPageUrl.indexOf("#") + 1);
- return String.format(CHAPTER_URL, id);
+ protected Request pageListRequest(String pageUrl) {
+ String id = pageUrl.substring(pageUrl.indexOf("#") + 1);
+ return ReqKt.get(String.format(CHAPTER_URL, id), requestHeaders);
}
@Override
- protected String overridePageUrl(String defaultPageUrl) {
- int start = defaultPageUrl.indexOf("#") + 1;
- int end = defaultPageUrl.indexOf("_", start);
- String id = defaultPageUrl.substring(start, end);
- return String.format(PAGE_URL, id, defaultPageUrl.substring(end+1));
+ protected Request imageUrlRequest(Page page) {
+ String pageUrl = page.getUrl();
+ int start = pageUrl.indexOf("#") + 1;
+ int end = pageUrl.indexOf("_", start);
+ String id = pageUrl.substring(start, end);
+ return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), requestHeaders);
}
private List parseMangasFromHtml(Document parsedHtml) {
@@ -318,7 +315,7 @@ public class Batoto extends LoginSource {
@Override
public Observable login(String username, String password) {
- return networkService.getStringResponse(LOGIN_URL, requestHeaders, false)
+ return networkService.requestBody(ReqKt.get(LOGIN_URL, requestHeaders))
.flatMap(response -> doLogin(response, username, password))
.map(this::isAuthenticationSuccessful);
}
@@ -337,7 +334,7 @@ public class Batoto extends LoginSource {
formBody.add("invisible", "1");
formBody.add("rememberMe", "1");
- return networkService.postData(postUrl, formBody.build(), requestHeaders);
+ return networkService.request(ReqKt.post(postUrl, requestHeaders, formBody.build()));
}
@Override
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java
index 75a9d78b8..50cbd1bf7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.java
@@ -17,15 +17,14 @@ import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.source.SourceManager;
+import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
-import okhttp3.Response;
-import rx.Observable;
+import okhttp3.Request;
public class Kissmanga extends Source {
@@ -52,11 +51,6 @@ public class Kissmanga extends Source {
return NAME;
}
- @Override
- public int getId() {
- return SourceManager.KISSMANGA;
- }
-
@Override
public String getBaseUrl() {
return BASE_URL;
@@ -72,6 +66,31 @@ public class Kissmanga extends Source {
return SEARCH_URL;
}
+ @Override
+ protected Request searchMangaRequest(MangasPage page, String query) {
+ if (page.page == 1) {
+ page.url = getInitialSearchUrl(query);
+ }
+
+ FormBody.Builder form = new FormBody.Builder();
+ form.add("authorArtist", "");
+ form.add("mangaName", query);
+ form.add("status", "");
+ form.add("genres", "");
+
+ return ReqKt.post(page.url, requestHeaders, form.build());
+ }
+
+ @Override
+ protected Request pageListRequest(String chapterUrl) {
+ return ReqKt.post(getBaseUrl() + chapterUrl, requestHeaders);
+ }
+
+ @Override
+ protected Request imageRequest(Page page) {
+ return ReqKt.get(page.getImageUrl());
+ }
+
@Override
protected List parsePopularMangasFromHtml(Document parsedHtml) {
List mangaList = new ArrayList<>();
@@ -104,25 +123,6 @@ public class Kissmanga extends Source {
return path != null ? BASE_URL + path : null;
}
- public Observable searchMangasFromNetwork(MangasPage page, String query) {
- if (page.page == 1)
- page.url = getInitialSearchUrl(query);
-
- FormBody.Builder form = new FormBody.Builder();
- form.add("authorArtist", "");
- form.add("mangaName", query);
- form.add("status", "");
- form.add("genres", "");
-
- return networkService
- .postData(page.url, form.build(), requestHeaders)
- .flatMap(networkService::mapResponseToString)
- .map(Jsoup::parse)
- .doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
- .doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
- .map(response -> page);
- }
-
@Override
protected List parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
@@ -195,19 +195,6 @@ public class Kissmanga extends Source {
return chapter;
}
- @Override
- public Observable> pullPageListFromNetwork(final String chapterUrl) {
- return networkService
- .postData(getBaseUrl() + overrideChapterUrl(chapterUrl), null, requestHeaders)
- .flatMap(networkService::mapResponseToString)
- .flatMap(unparsedHtml -> {
- List pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
- return !pages.isEmpty() ?
- Observable.just(parseFirstPage(pages, unparsedHtml)) :
- Observable.error(new Exception("Page list is empty"));
- });
- }
-
@Override
protected List parseHtmlToPageUrls(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
@@ -238,9 +225,4 @@ public class Kissmanga extends Source {
return null;
}
- @Override
- public Observable getImageProgressResponse(final Page page) {
- return networkService.getProgressResponse(page.getImageUrl(), null, page);
- }
-
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.java
index 4ec2abacc..a7b16f41b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.java
@@ -18,7 +18,6 @@ import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
@@ -40,11 +39,6 @@ public class Mangafox extends Source {
return NAME;
}
- @Override
- public int getId() {
- return SourceManager.MANGAFOX;
- }
-
@Override
public String getBaseUrl() {
return BASE_URL;
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.java b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.java
index 97c55092d..b545b7e8e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.java
@@ -18,7 +18,6 @@ import java.util.Locale;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.util.Parser;
@@ -39,11 +38,6 @@ public class Mangahere extends Source {
return NAME;
}
- @Override
- public int getId() {
- return SourceManager.MANGAHERE;
- }
-
@Override
public String getBaseUrl() {
return BASE_URL;
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/LibraryUpdateAlarm.java b/app/src/main/java/eu/kanade/tachiyomi/data/sync/LibraryUpdateAlarm.java
deleted file mode 100644
index 6c8df38fb..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/LibraryUpdateAlarm.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package eu.kanade.tachiyomi.data.sync;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
-
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import timber.log.Timber;
-
-public class LibraryUpdateAlarm extends BroadcastReceiver {
-
- public static final String LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY";
-
- public static void startAlarm(Context context) {
- startAlarm(context, PreferencesHelper.getLibraryUpdateInterval(context));
- }
-
- public static void startAlarm(Context context, int intervalInHours) {
- stopAlarm(context);
- if (intervalInHours == 0)
- return;
-
- int intervalInMillis = intervalInHours * 60 * 60 * 1000;
- long nextRun = SystemClock.elapsedRealtime() + intervalInMillis;
-
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- PendingIntent pendingIntent = getPendingIntent(context);
- alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- nextRun, intervalInMillis, pendingIntent);
-
- Timber.i("Alarm set. Library will update on " + nextRun);
- }
-
- public static void stopAlarm(Context context) {
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- PendingIntent pendingIntent = getPendingIntent(context);
- alarmManager.cancel(pendingIntent);
- }
-
- private static PendingIntent getPendingIntent(Context context) {
- Intent intent = new Intent(context, LibraryUpdateAlarm.class);
- intent.setAction(LIBRARY_UPDATE_ACTION);
- return PendingIntent.getBroadcast(context, 0, intent, 0);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction() == null)
- return;
-
- if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
- startAlarm(context);
- } else if (intent.getAction().equals(LIBRARY_UPDATE_ACTION)) {
- LibraryUpdateService.start(context);
- }
-
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/LibraryUpdateService.java b/app/src/main/java/eu/kanade/tachiyomi/data/sync/LibraryUpdateService.java
deleted file mode 100644
index eb9ae4460..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/LibraryUpdateService.java
+++ /dev/null
@@ -1,258 +0,0 @@
-package eu.kanade.tachiyomi.data.sync;
-
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.support.v4.app.NotificationCompat;
-import android.util.Pair;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.inject.Inject;
-
-import eu.kanade.tachiyomi.App;
-import eu.kanade.tachiyomi.BuildConfig;
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.database.DatabaseHelper;
-import eu.kanade.tachiyomi.data.database.models.Manga;
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import eu.kanade.tachiyomi.data.source.SourceManager;
-import eu.kanade.tachiyomi.ui.main.MainActivity;
-import eu.kanade.tachiyomi.util.AndroidComponentUtil;
-import eu.kanade.tachiyomi.util.NetworkUtil;
-import rx.Observable;
-import rx.Subscription;
-import rx.schedulers.Schedulers;
-import timber.log.Timber;
-
-public class LibraryUpdateService extends Service {
-
- @Inject DatabaseHelper db;
- @Inject SourceManager sourceManager;
- @Inject PreferencesHelper preferences;
-
- private PowerManager.WakeLock wakeLock;
- private Subscription subscription;
-
- public static final int UPDATE_NOTIFICATION_ID = 1;
-
- public static void start(Context context) {
- if (!isRunning(context)) {
- context.startService(getStartIntent(context));
- }
- }
-
- private static Intent getStartIntent(Context context) {
- return new Intent(context, LibraryUpdateService.class);
- }
-
- private static boolean isRunning(Context context) {
- return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService.class);
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- App.get(this).getComponent().inject(this);
- createAndAcquireWakeLock();
- }
-
- @Override
- public void onDestroy() {
- if (subscription != null)
- subscription.unsubscribe();
- // Reset the alarm
- LibraryUpdateAlarm.startAlarm(this);
- destroyWakeLock();
- super.onDestroy();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, final int startId) {
- Timber.i("Starting sync...");
-
- if (!NetworkUtil.isNetworkConnected(this)) {
- Timber.i("Sync canceled, connection not available");
- AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable.class, true);
- stopSelf(startId);
- return START_NOT_STICKY;
- }
-
- subscription = Observable.fromCallable(() -> db.getFavoriteMangas().executeAsBlocking())
- .subscribeOn(Schedulers.io())
- .flatMap(this::updateLibrary)
- .subscribe(next -> {},
- error -> {
- showNotification(getString(R.string.notification_update_error), "");
- stopSelf(startId);
- }, () -> {
- Timber.i("Library updated");
- stopSelf(startId);
- });
-
- return START_STICKY;
- }
-
- private Observable updateLibrary(List allLibraryMangas) {
- final AtomicInteger count = new AtomicInteger(0);
- final List updates = new ArrayList<>();
- final List failedUpdates = new ArrayList<>();
-
- final List mangas = !preferences.updateOnlyNonCompleted() ? allLibraryMangas :
- Observable.from(allLibraryMangas)
- .filter(manga -> manga.status != Manga.COMPLETED)
- .toList().toBlocking().single();
-
- return Observable.from(mangas)
- .doOnNext(manga -> showProgressNotification(
- getString(R.string.notification_update_progress,
- count.incrementAndGet(), mangas.size()), manga.title))
- .concatMap(manga -> updateManga(manga)
- .onErrorReturn(error -> {
- failedUpdates.add(manga);
- return Pair.create(0, 0);
- })
- // Filter out mangas without new chapters
- .filter(pair -> pair.first > 0)
- .map(pair -> new MangaUpdate(manga, pair.first)))
- .doOnNext(updates::add)
- .doOnCompleted(() -> {
- if (updates.isEmpty()) {
- cancelNotification();
- } else {
- showResultNotification(getString(R.string.notification_update_completed),
- getUpdatedMangasResult(updates, failedUpdates));
- }
- });
- }
-
- private Observable> updateManga(Manga manga) {
- return sourceManager.get(manga.source)
- .pullChaptersFromNetwork(manga.url)
- .flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters));
- }
-
- private String getUpdatedMangasResult(List updates, List failedUpdates) {
- final StringBuilder result = new StringBuilder();
- if (updates.isEmpty()) {
- result.append(getString(R.string.notification_no_new_chapters)).append("\n");
- } else {
- result.append(getString(R.string.notification_new_chapters));
-
- for (MangaUpdate update : updates) {
- result.append("\n").append(update.manga.title);
- }
- }
- if (!failedUpdates.isEmpty()) {
- result.append("\n");
- result.append(getString(R.string.notification_manga_update_failed));
- for (Manga manga : failedUpdates) {
- result.append("\n").append(manga.title);
- }
- }
-
- return result.toString();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- private void createAndAcquireWakeLock() {
- wakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
- PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock");
- wakeLock.acquire();
- }
-
- private void destroyWakeLock() {
- if (wakeLock != null && wakeLock.isHeld()) {
- wakeLock.release();
- wakeLock = null;
- }
- }
-
- private void showNotification(String title, String body) {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
- .setSmallIcon(R.drawable.ic_action_refresh)
- .setContentTitle(title)
- .setContentText(body);
-
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-
- notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
- }
-
- private void showProgressNotification(String title, String body) {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
- .setSmallIcon(R.drawable.ic_action_refresh)
- .setContentTitle(title)
- .setContentText(body)
- .setOngoing(true);
-
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-
- notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
- }
-
- private void showResultNotification(String title, String body) {
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
- .setSmallIcon(R.drawable.ic_action_refresh)
- .setContentTitle(title)
- .setStyle(new NotificationCompat.BigTextStyle().bigText(body))
- .setContentIntent(getNotificationIntent())
- .setAutoCancel(true);
-
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-
- notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
- }
-
- private void cancelNotification() {
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-
- notificationManager.cancel(UPDATE_NOTIFICATION_ID);
- }
-
- private PendingIntent getNotificationIntent() {
- Intent intent = new Intent(this, MainActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- public static class SyncOnConnectionAvailable extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (NetworkUtil.isNetworkConnected(context)) {
- if (BuildConfig.DEBUG) {
- Timber.i("Connection is now available, triggering sync...");
- }
- AndroidComponentUtil.toggleComponent(context, this.getClass(), false);
- context.startService(getStartIntent(context));
- }
- }
- }
-
- private static class MangaUpdate {
- public Manga manga;
- public int newChapters;
-
- public MangaUpdate(Manga manga, int newChapters) {
- this.manga = manga;
- this.newChapters = newChapters;
- }
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/UpdateMangaSyncService.java b/app/src/main/java/eu/kanade/tachiyomi/data/sync/UpdateMangaSyncService.java
deleted file mode 100644
index 3b291509e..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/UpdateMangaSyncService.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package eu.kanade.tachiyomi.data.sync;
-
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-
-import javax.inject.Inject;
-
-import eu.kanade.tachiyomi.App;
-import eu.kanade.tachiyomi.data.database.DatabaseHelper;
-import eu.kanade.tachiyomi.data.database.models.MangaSync;
-import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
-import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
-import rx.Observable;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.schedulers.Schedulers;
-import rx.subscriptions.CompositeSubscription;
-
-public class UpdateMangaSyncService extends Service {
-
- @Inject MangaSyncManager syncManager;
- @Inject DatabaseHelper db;
-
- private CompositeSubscription subscriptions;
-
- private static final String EXTRA_MANGASYNC = "extra_mangasync";
-
- public static void start(Context context, MangaSync mangaSync) {
- Intent intent = new Intent(context, UpdateMangaSyncService.class);
- intent.putExtra(EXTRA_MANGASYNC, mangaSync);
- context.startService(intent);
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- App.get(this).getComponent().inject(this);
- subscriptions = new CompositeSubscription();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- MangaSync mangaSync = (MangaSync) intent.getSerializableExtra(EXTRA_MANGASYNC);
- updateLastChapterRead(mangaSync, startId);
- return START_STICKY;
- }
-
- @Override
- public void onDestroy() {
- subscriptions.unsubscribe();
- super.onDestroy();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- private void updateLastChapterRead(MangaSync mangaSync, int startId) {
- MangaSyncService sync = syncManager.getSyncService(mangaSync.sync_id);
-
- subscriptions.add(Observable.defer(() -> sync.update(mangaSync))
- .flatMap(response -> {
- if (response.isSuccessful()) {
- return db.insertMangaSync(mangaSync).asRxObservable();
- }
- return Observable.error(new Exception("Could not update MAL"));
- })
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- stopSelf(startId);
- }, error -> {
- stopSelf(startId);
- }));
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt
new file mode 100644
index 000000000..400b46c89
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubRelease.kt
@@ -0,0 +1,30 @@
+package eu.kanade.tachiyomi.data.updater
+
+import com.google.gson.annotations.SerializedName
+
+/**
+ * Release object.
+ * Contains information about the latest release from Github.
+ *
+ * @param version version of latest release.
+ * @param changeLog log of latest release.
+ * @param assets assets of latest release.
+ */
+class GithubRelease(@SerializedName("tag_name") val version: String,
+ @SerializedName("body") val changeLog: String,
+ @SerializedName("assets") val assets: List) {
+
+ /**
+ * Get download link of latest release from the assets.
+ * @return download link of latest release.
+ */
+ val downloadLink: String
+ get() = assets[0].downloadLink
+
+ /**
+ * Assets class containing download url.
+ * @param downloadLink download url.
+ */
+ inner class Assets(@SerializedName("browser_download_url") val downloadLink: String)
+}
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt
new file mode 100644
index 000000000..7bce4082b
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt
@@ -0,0 +1,30 @@
+package eu.kanade.tachiyomi.data.updater
+
+import retrofit2.Retrofit
+import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.http.GET
+import rx.Observable
+
+
+/**
+ * Used to connect with the Github API.
+ */
+interface GithubService {
+
+ companion object {
+ fun create(): GithubService {
+ val restAdapter = Retrofit.Builder()
+ .baseUrl("https://api.github.com")
+ .addConverterFactory(GsonConverterFactory.create())
+ .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
+ .build()
+
+ return restAdapter.create(GithubService::class.java)
+ }
+ }
+
+ @GET("/repos/inorichi/tachiyomi/releases/latest")
+ fun getLatestVersion(): Observable
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt
new file mode 100644
index 000000000..306fab71b
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt
@@ -0,0 +1,20 @@
+package eu.kanade.tachiyomi.data.updater
+
+import android.content.Context
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.toast
+import rx.Observable
+
+
+class GithubUpdateChecker(private val context: Context) {
+
+ val service: GithubService = GithubService.create()
+
+ /**
+ * Returns observable containing release information
+ */
+ fun checkForApplicationUpdate(): Observable {
+ context.toast(R.string.update_check_look_for_updates)
+ return service.getLatestVersion()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.java b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.java
deleted file mode 100644
index 1f12d8f10..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package eu.kanade.tachiyomi.data.updater;
-
-
-import android.content.Context;
-
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.rest.GithubService;
-import eu.kanade.tachiyomi.data.rest.Release;
-import eu.kanade.tachiyomi.data.rest.ServiceFactory;
-import eu.kanade.tachiyomi.util.ToastUtil;
-import rx.Observable;
-
-
-public class UpdateChecker {
- private final Context context;
-
- public UpdateChecker(Context context) {
- this.context = context;
- }
-
- /**
- * Returns observable containing release information
- *
- */
- public Observable checkForApplicationUpdate() {
- ToastUtil.showShort(context, context.getString(R.string.update_check_look_for_updates));
- //Create Github service to retrieve Github data
- GithubService service = ServiceFactory.createRetrofitService(GithubService.class, GithubService.SERVICE_ENDPOINT);
- return service.getLatestVersion();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java b/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java
index 99b76491c..b45fb246b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.java
@@ -6,10 +6,10 @@ import javax.inject.Singleton;
import dagger.Component;
import eu.kanade.tachiyomi.data.download.DownloadService;
-import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
+import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.source.base.Source;
-import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
-import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService;
+import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService;
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
import eu.kanade.tachiyomi.injection.module.AppModule;
import eu.kanade.tachiyomi.injection.module.DataModule;
@@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaPresenter;
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter;
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter;
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter;
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter;
import eu.kanade.tachiyomi.ui.recent.RecentChaptersPresenter;
import eu.kanade.tachiyomi.ui.setting.SettingsAccountsFragment;
@@ -48,15 +47,13 @@ public interface AppComponent {
void inject(CategoryPresenter categoryPresenter);
void inject(RecentChaptersPresenter recentChaptersPresenter);
- void inject(ReaderActivity readerActivity);
void inject(MangaActivity mangaActivity);
void inject(SettingsAccountsFragment settingsAccountsFragment);
void inject(SettingsActivity settingsActivity);
void inject(Source source);
-
- void inject(MyAnimeList myAnimeList);
+ void inject(MangaSyncService mangaSyncService);
void inject(LibraryUpdateService libraryUpdateService);
void inject(DownloadService downloadService);
diff --git a/app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.java b/app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.java
index 3aa9cf97a..d516fe584 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/injection/module/DataModule.java
@@ -29,7 +29,7 @@ public class DataModule {
@Provides
@Singleton
- DatabaseHelper provideDatabaseHelper(Application app) {
+ public DatabaseHelper provideDatabaseHelper(Application app) {
return new DatabaseHelper(app);
}
@@ -47,13 +47,13 @@ public class DataModule {
@Provides
@Singleton
- NetworkHelper provideNetworkHelper(Application app) {
+ public NetworkHelper provideNetworkHelper(Application app) {
return new NetworkHelper(app);
}
@Provides
@Singleton
- SourceManager provideSourceManager(Application app) {
+ public SourceManager provideSourceManager(Application app) {
return new SourceManager(app);
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java
index 0374c7a59..217600448 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java
@@ -35,7 +35,7 @@ import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.io.IOHandler;
-import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService;
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.library.category.CategoryActivity;
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java
index aede73ffd..dbfb5bf2d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java
@@ -20,12 +20,12 @@ import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.download.DownloadManager;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
+import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
import eu.kanade.tachiyomi.event.ReaderEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import icepick.State;
@@ -348,7 +348,7 @@ public class ReaderPresenter extends BasePresenter {
public void updateMangaSyncLastChapterRead() {
for (MangaSync mangaSync : mangaSyncList) {
- MangaSyncService service = syncManager.getSyncService(mangaSync.sync_id);
+ MangaSyncService service = syncManager.getService(mangaSync.sync_id);
if (service.isLogged() && mangaSync.update) {
UpdateMangaSyncService.start(getContext(), mangaSync);
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.java
index 4fe669d99..0562a5871 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.java
@@ -17,7 +17,7 @@ import java.util.TimeZone;
import eu.kanade.tachiyomi.BuildConfig;
import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.updater.UpdateChecker;
+import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker;
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Subscription;
@@ -28,7 +28,7 @@ public class SettingsAboutFragment extends SettingsNestedFragment {
/**
* Checks for new releases
*/
- private UpdateChecker updateChecker;
+ private GithubUpdateChecker updateChecker;
/**
* The subscribtion service of the obtained release object
@@ -44,7 +44,7 @@ public class SettingsAboutFragment extends SettingsNestedFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
//Check for update
- updateChecker = new UpdateChecker(getActivity());
+ updateChecker = new GithubUpdateChecker(getActivity());
super.onCreate(savedInstanceState);
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAccountsFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAccountsFragment.java
index b705fbc50..ae8440c2c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAccountsFragment.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAccountsFragment.java
@@ -60,7 +60,7 @@ public class SettingsAccountsFragment extends SettingsNestedFragment {
mangaSyncCategory.setTitle("Sync");
screen.addPreference(mangaSyncCategory);
- for (MangaSyncService sync : syncManager.getSyncServices()) {
+ for (MangaSyncService sync : syncManager.getServices()) {
MangaSyncLoginDialog dialog = new MangaSyncLoginDialog(
screen.getContext(), preferences, sync);
dialog.setTitle(sync.getName());
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.java
index f5089e699..e3a51a60b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.java
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.java
@@ -7,7 +7,7 @@ import android.view.ViewGroup;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import eu.kanade.tachiyomi.data.sync.LibraryUpdateAlarm;
+import eu.kanade.tachiyomi.data.library.LibraryUpdateAlarm;
import eu.kanade.tachiyomi.widget.preference.IntListPreference;
import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog;
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
new file mode 100644
index 000000000..c32083918
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
@@ -0,0 +1,35 @@
+package eu.kanade.tachiyomi.util
+
+import android.app.AlarmManager
+import android.app.Notification
+import android.content.Context
+import android.support.annotation.StringRes
+import android.support.v4.app.NotificationCompat
+import android.widget.Toast
+
+/**
+ * Display a toast in this context.
+ * @param resource the text resource.
+ * @param duration the duration of the toast. Defaults to short.
+ */
+fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) {
+ Toast.makeText(this, resource, duration).show()
+}
+
+/**
+ * Helper method to create a notification.
+ * @param func the function that will execute inside the builder.
+ * @return a notification to be displayed or updated.
+ */
+inline fun Context.notification(func: NotificationCompat.Builder.() -> Unit): Notification {
+ val builder = NotificationCompat.Builder(this)
+ builder.func()
+ return builder.build()
+}
+
+/**
+ * Property to get the alarm manager from the context.
+ * @return the alarm manager.
+ */
+val Context.alarmManager: AlarmManager
+ get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt
new file mode 100644
index 000000000..be971001e
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/JsoupExtensions.kt
@@ -0,0 +1,12 @@
+package eu.kanade.tachiyomi.util
+
+import org.jsoup.nodes.Element
+
+fun Element.selectText(css: String, defaultValue: String? = null): String? {
+ return select(css).first()?.text() ?: defaultValue
+}
+
+fun Element.selectInt(css: String, defaultValue: Int = 0): Int {
+ return select(css).first()?.text()?.toInt() ?: defaultValue
+}
+
diff --git a/app/src/test/java/eu/kanade/tachiyomi/CustomBuildConfig.java b/app/src/test/java/eu/kanade/tachiyomi/CustomBuildConfig.java
new file mode 100644
index 000000000..de3ec0ad7
--- /dev/null
+++ b/app/src/test/java/eu/kanade/tachiyomi/CustomBuildConfig.java
@@ -0,0 +1,15 @@
+package eu.kanade.tachiyomi;
+
+public class CustomBuildConfig {
+ public static final boolean DEBUG = Boolean.parseBoolean("true");
+ public static final String APPLICATION_ID = "eu.kanade.tachiyomi";
+ public static final String BUILD_TYPE = "debug";
+ public static final String FLAVOR = "";
+ public static final int VERSION_CODE = 4;
+ public static final String VERSION_NAME = "0.1.3";
+ // Fields from default config.
+ public static final String BUILD_TIME = "2016-02-19T14:49Z";
+ public static final String COMMIT_COUNT = "482";
+ public static final String COMMIT_SHA = "e52c498";
+ public static final boolean INCLUDE_UPDATER = true;
+}
diff --git a/app/src/test/java/eu/kanade/tachiyomi/TestApp.java b/app/src/test/java/eu/kanade/tachiyomi/TestApp.java
index ba670b624..451db6e41 100644
--- a/app/src/test/java/eu/kanade/tachiyomi/TestApp.java
+++ b/app/src/test/java/eu/kanade/tachiyomi/TestApp.java
@@ -1,9 +1,24 @@
package eu.kanade.tachiyomi;
+import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
+import eu.kanade.tachiyomi.injection.module.AppModule;
+
public class TestApp extends App {
+ @Override
+ protected DaggerAppComponent.Builder prepareAppComponent() {
+ return DaggerAppComponent.builder()
+ .appModule(new AppModule(this))
+ .dataModule(new TestDataModule());
+ }
+
@Override
protected void setupEventBus() {
// Do nothing
}
+
+ @Override
+ protected void setupAcra() {
+ // Do nothing
+ }
}
diff --git a/app/src/test/java/eu/kanade/tachiyomi/TestDataModule.java b/app/src/test/java/eu/kanade/tachiyomi/TestDataModule.java
new file mode 100644
index 000000000..803386e69
--- /dev/null
+++ b/app/src/test/java/eu/kanade/tachiyomi/TestDataModule.java
@@ -0,0 +1,29 @@
+package eu.kanade.tachiyomi;
+
+import android.app.Application;
+
+import org.mockito.Mockito;
+
+import eu.kanade.tachiyomi.data.database.DatabaseHelper;
+import eu.kanade.tachiyomi.data.network.NetworkHelper;
+import eu.kanade.tachiyomi.data.source.SourceManager;
+import eu.kanade.tachiyomi.injection.module.DataModule;
+
+public class TestDataModule extends DataModule {
+
+ @Override
+ public DatabaseHelper provideDatabaseHelper(Application app) {
+ return Mockito.mock(DatabaseHelper.class, Mockito.RETURNS_DEEP_STUBS);
+ }
+
+ @Override
+ public NetworkHelper provideNetworkHelper(Application app) {
+ return Mockito.mock(NetworkHelper.class);
+ }
+
+ @Override
+ public SourceManager provideSourceManager(Application app) {
+ return Mockito.mock(SourceManager.class, Mockito.RETURNS_DEEP_STUBS);
+ }
+
+}
diff --git a/app/src/test/java/eu/kanade/tachiyomi/UseModule.java b/app/src/test/java/eu/kanade/tachiyomi/UseModule.java
deleted file mode 100644
index 3cab866a9..000000000
--- a/app/src/test/java/eu/kanade/tachiyomi/UseModule.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package eu.kanade.tachiyomi;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Created by len on 1/10/15.
- */
-
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-public @interface UseModule {
- Class value();
-}
\ No newline at end of file
diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java
new file mode 100644
index 000000000..02ffadb63
--- /dev/null
+++ b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java
@@ -0,0 +1,140 @@
+package eu.kanade.tachiyomi.data.library;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.SystemClock;
+
+import org.assertj.core.data.Offset;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlarmManager;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowPendingIntent;
+
+import eu.kanade.tachiyomi.CustomBuildConfig;
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.spy;
+import static org.robolectric.Shadows.shadowOf;
+
+@Config(constants = CustomBuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
+@RunWith(RobolectricGradleTestRunner.class)
+public class LibraryUpdateAlarmTest {
+
+ ShadowApplication app;
+ Context context;
+ ShadowAlarmManager alarmManager;
+
+ @Before
+ public void setup() {
+ app = ShadowApplication.getInstance();
+ context = spy(app.getApplicationContext());
+
+ alarmManager = shadowOf((AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
+ }
+
+ @Test
+ public void testLibraryIntentHandling() {
+ Intent intent = new Intent(LibraryUpdateAlarm.LIBRARY_UPDATE_ACTION);
+ assertThat(app.hasReceiverForIntent(intent)).isTrue();
+ }
+
+ @Test
+ public void testAlarmIsNotStarted() {
+ assertThat(alarmManager.getNextScheduledAlarm()).isNull();
+ }
+
+ @Test
+ public void testAlarmIsNotStartedWhenBootReceivedAndSettingZero() {
+ LibraryUpdateAlarm alarm = new LibraryUpdateAlarm();
+ alarm.onReceive(context, new Intent(Intent.ACTION_BOOT_COMPLETED));
+
+ assertThat(alarmManager.getNextScheduledAlarm()).isNull();
+ }
+
+ @Test
+ public void testAlarmIsStartedWhenBootReceivedAndSettingNotZero() {
+ PreferencesHelper prefs = new PreferencesHelper(context);
+ prefs.libraryUpdateInterval().set(1);
+
+ LibraryUpdateAlarm alarm = new LibraryUpdateAlarm();
+ alarm.onReceive(context, new Intent(Intent.ACTION_BOOT_COMPLETED));
+
+ assertThat(alarmManager.getNextScheduledAlarm()).isNotNull();
+ }
+
+ @Test
+ public void testOnlyOneAlarmExists() {
+ PreferencesHelper prefs = new PreferencesHelper(context);
+ prefs.libraryUpdateInterval().set(1);
+
+ LibraryUpdateAlarm.startAlarm(context);
+ LibraryUpdateAlarm.startAlarm(context);
+ LibraryUpdateAlarm.startAlarm(context);
+
+ assertThat(alarmManager.getScheduledAlarms()).hasSize(1);
+ }
+
+ @Test
+ public void testLibraryWillBeUpdatedWhenAlarmFired() {
+ PreferencesHelper prefs = new PreferencesHelper(context);
+ prefs.libraryUpdateInterval().set(1);
+
+ Intent expectedIntent = new Intent(context, LibraryUpdateAlarm.class);
+ expectedIntent.setAction(LibraryUpdateAlarm.LIBRARY_UPDATE_ACTION);
+
+ LibraryUpdateAlarm.startAlarm(context);
+
+ ShadowAlarmManager.ScheduledAlarm scheduledAlarm = alarmManager.getNextScheduledAlarm();
+ ShadowPendingIntent pendingIntent = shadowOf(scheduledAlarm.operation);
+ assertThat(pendingIntent.isBroadcastIntent()).isTrue();
+ assertThat(pendingIntent.getSavedIntents()).hasSize(1);
+ assertThat(expectedIntent.getComponent()).isEqualTo(pendingIntent.getSavedIntents()[0].getComponent());
+ assertThat(expectedIntent.getAction()).isEqualTo(pendingIntent.getSavedIntents()[0].getAction());
+ }
+
+ @Test
+ public void testLibraryUpdateServiceIsStartedWhenUpdateIntentIsReceived() {
+ Intent intent = new Intent(context, LibraryUpdateService.class);
+ assertThat(app.getNextStartedService()).isNotEqualTo(intent);
+
+ LibraryUpdateAlarm alarm = new LibraryUpdateAlarm();
+ alarm.onReceive(context, new Intent(LibraryUpdateAlarm.LIBRARY_UPDATE_ACTION));
+
+ assertThat(app.getNextStartedService()).isEqualTo(intent);
+ }
+
+ @Test
+ public void testReceiverDoesntReactToNullActions() {
+ PreferencesHelper prefs = new PreferencesHelper(context);
+ prefs.libraryUpdateInterval().set(1);
+
+ Intent intent = new Intent(context, LibraryUpdateService.class);
+
+ LibraryUpdateAlarm alarm = new LibraryUpdateAlarm();
+ alarm.onReceive(context, new Intent());
+
+ assertThat(app.getNextStartedService()).isNotEqualTo(intent);
+ assertThat(alarmManager.getScheduledAlarms()).hasSize(0);
+ }
+
+ @Test
+ public void testAlarmFiresCloseToDesiredTime() {
+ int hours = 2;
+ LibraryUpdateAlarm.startAlarm(context, hours);
+
+ long shouldRunAt = SystemClock.elapsedRealtime() + (hours * 60 * 60 * 1000);
+
+ // Margin error of 3 seconds
+ Offset offset = Offset.offset(3 * 1000L);
+
+ assertThat(alarmManager.getNextScheduledAlarm().triggerAtTime).isCloseTo(shouldRunAt, offset);
+ }
+
+}
diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java
new file mode 100644
index 000000000..720652237
--- /dev/null
+++ b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java
@@ -0,0 +1,130 @@
+package eu.kanade.tachiyomi.data.library;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.util.Pair;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.kanade.tachiyomi.CustomBuildConfig;
+import eu.kanade.tachiyomi.data.database.models.Chapter;
+import eu.kanade.tachiyomi.data.database.models.Manga;
+import eu.kanade.tachiyomi.data.source.base.Source;
+import rx.Observable;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@Config(constants = CustomBuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
+@RunWith(RobolectricGradleTestRunner.class)
+public class LibraryUpdateServiceTest {
+
+ ShadowApplication app;
+ Context context;
+ LibraryUpdateService service;
+ Source source;
+
+ @Before
+ public void setup() {
+ app = ShadowApplication.getInstance();
+ context = app.getApplicationContext();
+ service = Robolectric.setupService(LibraryUpdateService.class);
+ source = mock(Source.class);
+ when(service.sourceManager.get(anyInt())).thenReturn(source);
+ }
+
+ @Test
+ public void testStartCommand() {
+ service.onStartCommand(new Intent(), 0, 0);
+ verify(service.db).getFavoriteMangas();
+ }
+
+ @Test
+ public void testLifecycle() {
+ // Smoke test
+ Robolectric.buildService(LibraryUpdateService.class)
+ .attach()
+ .create()
+ .startCommand(0, 0)
+ .destroy()
+ .get();
+ }
+
+ @Test
+ public void testUpdateManga() {
+ Manga manga = Manga.create("manga1");
+ List chapters = createChapters("/chapter1", "/chapter2");
+
+ when(source.pullChaptersFromNetwork(manga.url)).thenReturn(Observable.just(chapters));
+ when(service.db.insertOrRemoveChapters(manga, chapters))
+ .thenReturn(Observable.just(Pair.create(2, 0)));
+
+ service.updateManga(manga).subscribe();
+
+ verify(service.db).insertOrRemoveChapters(manga, chapters);
+ }
+
+ @Test
+ public void testContinuesUpdatingWhenAMangaFails() {
+ Manga manga1 = Manga.create("manga1");
+ Manga manga2 = Manga.create("manga2");
+ Manga manga3 = Manga.create("manga3");
+
+ List favManga = createManga("manga1", "manga2", "manga3");
+
+ List chapters = createChapters("/chapter1", "/chapter2");
+ List chapters3 = createChapters("/achapter1", "/achapter2");
+
+ when(service.db.getFavoriteMangas().executeAsBlocking()).thenReturn(favManga);
+
+ // One of the updates will fail
+ when(source.pullChaptersFromNetwork("manga1")).thenReturn(Observable.just(chapters));
+ when(source.pullChaptersFromNetwork("manga2")).thenReturn(Observable.error(new Exception()));
+ when(source.pullChaptersFromNetwork("manga3")).thenReturn(Observable.just(chapters3));
+
+ when(service.db.insertOrRemoveChapters(manga1, chapters)).thenReturn(Observable.just(Pair.create(2, 0)));
+ when(service.db.insertOrRemoveChapters(manga3, chapters)).thenReturn(Observable.just(Pair.create(2, 0)));
+
+ service.updateLibrary().subscribe();
+
+ // There are 3 network attempts and 2 insertions (1 request failed)
+ verify(source, times(3)).pullChaptersFromNetwork(any());
+ verify(service.db, times(2)).insertOrRemoveChapters(any(), any());
+ verify(service.db, never()).insertOrRemoveChapters(eq(manga2), any());
+ }
+
+ private List createChapters(String... urls) {
+ List list = new ArrayList<>();
+ for (String url : urls) {
+ Chapter c = Chapter.create();
+ c.url = url;
+ list.add(c);
+ }
+ return list;
+ }
+
+ private List createManga(String... urls) {
+ List list = new ArrayList<>();
+ for (String url : urls) {
+ Manga m = Manga.create(url);
+ list.add(m);
+ }
+ return list;
+ }
+}
diff --git a/build.gradle b/build.gradle
index 11eb25d59..ff86c05a2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.0.0-beta2'
+ classpath 'com.android.tools.build:gradle:2.0.0-beta5'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'me.tatarka:gradle-retrolambda:3.2.4'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'