diff --git a/app/build.gradle b/app/build.gradle
index 3c59b64b89..3ad59e14ea 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 873e9f4022..ed8a82fa7b 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 63dfa0cc7d..3992bffd49 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 41b46df340..0000000000
--- 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 0000000000..1ff58e6f30
--- /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 17ede81228..0000000000
--- 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 0000000000..fb78a4f31a
--- /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 c02cf9ce79..0000000000
--- 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 0000000000..3e7504802d
--- /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 0000000000..8f2368406c
--- /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 0000000000..fa71ca68c4
--- /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 3411b852b1..0000000000
--- 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 0000000000..54a29975af
--- /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 0000000000..81f0c74598
--- /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 1445447fc2..0000000000
--- 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 0000000000..723abdc518
--- /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 add73acc9c..0000000000
--- 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 0000000000..ab91b30503
--- /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 4ecbbaae62..0000000000
--- 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 0000000000..05225aad5b
--- /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 ae43b27f77..0000000000
--- 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 0000000000..f624e2b621
--- /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 b74016bc42..0000000000
--- 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 0000000000..67c639b1ae
--- /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 0000000000..0cedb2e970
--- /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 4e93f17681..342481f1f8 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 2f34f6b5b8..0000000000
--- 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 b0f2398cbe..0000000000
--- 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 6cf455fda7..0000000000
--- 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 2a7d183f15..0000000000
--- 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