mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 21:01:50 +01:00
Merge pull request #169 from inorichi/kotlin
Partial migration of data package to Kotlin
This commit is contained in:
commit
db97250db8
@ -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()
|
||||
}
|
||||
|
@ -51,17 +51,17 @@
|
||||
android:theme="@style/FilePickerTheme">
|
||||
</activity>
|
||||
|
||||
<service android:name=".data.sync.LibraryUpdateService"
|
||||
<service android:name=".data.library.LibraryUpdateService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service android:name=".data.download.DownloadService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service android:name=".data.sync.UpdateMangaSyncService"
|
||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver
|
||||
android:name=".data.sync.LibraryUpdateService$SyncOnConnectionAvailable"
|
||||
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
@ -69,7 +69,7 @@
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".data.sync.LibraryUpdateAlarm">
|
||||
android:name=".data.library.LibraryUpdateAlarm">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="eu.kanade.UPDATE_LIBRARY" />
|
||||
|
@ -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<AppComponent> getComponentReflection() {
|
||||
|
@ -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<List<Page>>() {}.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<List<Page>> 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<Page> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
213
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
vendored
Normal file
213
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
vendored
Normal file
@ -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<List<Page>>() {}.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<List<Page>> {
|
||||
return Observable.fromCallable<List<Page>> {
|
||||
// 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<Page>) {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<File>() {
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
158
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
vendored
Normal file
158
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
vendored
Normal file
@ -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<File>() {
|
||||
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
@ -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!
|
||||
}
|
||||
}
|
22
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
vendored
Normal file
22
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
vendored
Normal file
@ -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!
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Manga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
val count = AtomicInteger(0)
|
||||
val newUpdates = ArrayList<Manga>()
|
||||
val failedUpdates = ArrayList<Manga>()
|
||||
|
||||
// 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<Pair<Int, Int>> {
|
||||
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<Manga>, failedUpdates: List<Manga>): 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<Manga>, failed: List<Manga>) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<MangaSyncService> 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<MangaSyncService> getSyncServices() {
|
||||
return services;
|
||||
}
|
||||
|
||||
public MangaSyncService getSyncService(int id) {
|
||||
switch (id) {
|
||||
case MYANIMELIST:
|
||||
return myAnimeList;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -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<MangaSyncService>
|
||||
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 }!!
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Boolean> login(String username, String password);
|
||||
|
||||
public abstract boolean isLogged();
|
||||
|
||||
public abstract Observable<Response> update(MangaSync manga);
|
||||
|
||||
public abstract Observable<Response> add(MangaSync manga);
|
||||
|
||||
public abstract Observable<Response> bind(MangaSync manga);
|
||||
|
||||
public abstract String getStatus(int status);
|
||||
|
||||
}
|
@ -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<Boolean>
|
||||
|
||||
open val isLogged: Boolean
|
||||
get() = !preferences.getMangaSyncUsername(this).isEmpty() &&
|
||||
!preferences.getMangaSyncPassword(this).isEmpty()
|
||||
|
||||
abstract fun update(manga: MangaSync): Observable<Response>
|
||||
|
||||
abstract fun add(manga: MangaSync): Observable<Response>
|
||||
|
||||
abstract fun bind(manga: MangaSync): Observable<Response>
|
||||
|
||||
abstract fun getStatus(status: Int): String
|
||||
|
||||
}
|
@ -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<Boolean> 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<List<MangaSync>> 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<List<MangaSync>> 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<Response> 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<Response> 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<Response> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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<Boolean> {
|
||||
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<List<MangaSync>> {
|
||||
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<List<MangaSync>> {
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -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<Response> 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<String> mapResponseToString(final Response response) {
|
||||
return Observable.defer(() -> {
|
||||
try {
|
||||
return Observable.just(response.body().string());
|
||||
} catch (Throwable e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<String> getStringResponse(final String url, final Headers headers, boolean forceCache) {
|
||||
return getResponse(url, headers, forceCache)
|
||||
.flatMap(this::mapResponseToString);
|
||||
}
|
||||
|
||||
public Observable<Response> 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<Response> 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();
|
||||
}
|
||||
|
||||
}
|
@ -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<Response> {
|
||||
return Observable.fromCallable {
|
||||
val c = if (forceCache) forceCacheClient else client
|
||||
c.newCall(request).execute()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
|
||||
return request(request, forceCache)
|
||||
.map { it.body().string() }
|
||||
}
|
||||
|
||||
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
|
||||
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
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.network;
|
||||
|
||||
public interface ProgressListener {
|
||||
void update(long bytesRead, long contentLength, boolean done);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
interface ProgressListener {
|
||||
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
app/src/main/java/eu/kanade/tachiyomi/data/network/Req.kt
Normal file
34
app/src/main/java/eu/kanade/tachiyomi/data/network/Req.kt
Normal file
@ -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()
|
||||
}
|
@ -190,4 +190,8 @@ public class PreferencesHelper {
|
||||
context.getString(R.string.pref_library_update_interval_key), 0);
|
||||
}
|
||||
|
||||
public Preference<Integer> libraryUpdateInterval() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_library_update_interval_key), 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<Release> getLatestVersion();
|
||||
|
||||
}
|
@ -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> 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> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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> T createRetrofitService(final Class<T> clazz, final String endPoint) {
|
||||
final RestAdapter restAdapter = new RestAdapter.Builder()
|
||||
.setEndpoint(endPoint)
|
||||
.build();
|
||||
|
||||
return restAdapter.create(clazz);
|
||||
}
|
||||
}
|
@ -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<Integer, Source> 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<Source> getSources() {
|
||||
List<Source> sources = new ArrayList<>(sourcesMap.values());
|
||||
Collections.sort(sources, (s1, s2) -> s1.getName().compareTo(s2.getName()));
|
||||
return sources;
|
||||
}
|
||||
|
||||
}
|
@ -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<Int, Source>
|
||||
val sources: List<Source>
|
||||
|
||||
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<Int, Source> {
|
||||
val map = HashMap<Int, Source>()
|
||||
for (i in 1..LAST_SOURCE) {
|
||||
val source = createSource(i)
|
||||
if (source != null) {
|
||||
source.id = i
|
||||
map.put(i, source)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
||||
@ -69,24 +77,6 @@ public abstract class BaseSource {
|
||||
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() {
|
||||
Headers.Builder builder = new Headers.Builder();
|
||||
|
@ -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<MangasPage> 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<MangasPage> 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<Manga> 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<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, false)
|
||||
.requestBody(chapterListRequest(mangaUrl))
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Chapter> chapters = parseHtmlToChapters(unparsedHtml);
|
||||
return !chapters.isEmpty() ?
|
||||
@ -102,7 +134,7 @@ public abstract class Source extends BaseSource {
|
||||
|
||||
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, false)
|
||||
.requestBody(pageListRequest(chapterUrl))
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
|
||||
return !pages.isEmpty() ?
|
||||
@ -127,7 +159,7 @@ public abstract class Source extends BaseSource {
|
||||
public Observable<Page> 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<Response> getImageProgressResponse(final Page page) {
|
||||
return networkService.getProgressResponse(page.getImageUrl(), requestHeaders, page);
|
||||
return networkService.requestBodyProgress(imageRequest(page), page);
|
||||
}
|
||||
|
||||
public void savePageList(String chapterUrl, List<Page> pages) {
|
||||
|
@ -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<Manga> parseMangasFromHtml(Document parsedHtml) {
|
||||
@ -318,7 +315,7 @@ public class Batoto extends LoginSource {
|
||||
|
||||
@Override
|
||||
public Observable<Boolean> 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
|
||||
|
@ -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<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
List<Manga> mangaList = new ArrayList<>();
|
||||
@ -104,25 +123,6 @@ public class Kissmanga extends Source {
|
||||
return path != null ? BASE_URL + path : null;
|
||||
}
|
||||
|
||||
public Observable<MangasPage> 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<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
return parsePopularMangasFromHtml(parsedHtml);
|
||||
@ -195,19 +195,6 @@ public class Kissmanga extends Source {
|
||||
return chapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
|
||||
return networkService
|
||||
.postData(getBaseUrl() + overrideChapterUrl(chapterUrl), null, requestHeaders)
|
||||
.flatMap(networkService::mapResponseToString)
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
|
||||
return !pages.isEmpty() ?
|
||||
Observable.just(parseFirstPage(pages, unparsedHtml)) :
|
||||
Observable.error(new Exception("Page list is empty"));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
@ -238,9 +225,4 @@ public class Kissmanga extends Source {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable<Response> getImageProgressResponse(final Page page) {
|
||||
return networkService.getProgressResponse(page.getImageUrl(), null, page);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<MangaUpdate> updateLibrary(List<Manga> allLibraryMangas) {
|
||||
final AtomicInteger count = new AtomicInteger(0);
|
||||
final List<MangaUpdate> updates = new ArrayList<>();
|
||||
final List<Manga> failedUpdates = new ArrayList<>();
|
||||
|
||||
final List<Manga> 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<Pair<Integer, Integer>> updateManga(Manga manga) {
|
||||
return sourceManager.get(manga.source)
|
||||
.pullChaptersFromNetwork(manga.url)
|
||||
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters));
|
||||
}
|
||||
|
||||
private String getUpdatedMangasResult(List<MangaUpdate> updates, List<Manga> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
@ -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<Assets>) {
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
@ -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<GithubRelease>
|
||||
|
||||
}
|
@ -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<GithubRelease> {
|
||||
context.toast(R.string.update_check_look_for_updates)
|
||||
return service.getLatestVersion()
|
||||
}
|
||||
}
|
@ -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<Release> 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<ReaderActivity> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
|
15
app/src/test/java/eu/kanade/tachiyomi/CustomBuildConfig.java
Normal file
15
app/src/test/java/eu/kanade/tachiyomi/CustomBuildConfig.java
Normal file
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
29
app/src/test/java/eu/kanade/tachiyomi/TestDataModule.java
Normal file
29
app/src/test/java/eu/kanade/tachiyomi/TestDataModule.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
@ -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<Long> offset = Offset.offset(3 * 1000L);
|
||||
|
||||
assertThat(alarmManager.getNextScheduledAlarm().triggerAtTime).isCloseTo(shouldRunAt, offset);
|
||||
}
|
||||
|
||||
}
|
@ -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<Chapter> 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<Manga> favManga = createManga("manga1", "manga2", "manga3");
|
||||
|
||||
List<Chapter> chapters = createChapters("/chapter1", "/chapter2");
|
||||
List<Chapter> 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<Chapter> createChapters(String... urls) {
|
||||
List<Chapter> list = new ArrayList<>();
|
||||
for (String url : urls) {
|
||||
Chapter c = Chapter.create();
|
||||
c.url = url;
|
||||
list.add(c);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<Manga> createManga(String... urls) {
|
||||
List<Manga> list = new ArrayList<>();
|
||||
for (String url : urls) {
|
||||
Manga m = Manga.create(url);
|
||||
list.add(m);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user