mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Partial migration of data package to Kotlin
This commit is contained in:
		| @@ -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(); | ||||
|  | ||||
| @@ -68,24 +76,6 @@ public abstract class BaseSource { | ||||
|     protected boolean isAuthenticationSuccessful(Response response) { | ||||
|         throw new UnsupportedOperationException("Not implemented"); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // Default fields, they can be overriden by sources' implementation | ||||
|  | ||||
|     // Get the URL to the details of a manga, useful if the source provides some kind of API or fast calls | ||||
|     protected String overrideMangaUrl(String defaultMangaUrl) { | ||||
|         return defaultMangaUrl; | ||||
|     } | ||||
|  | ||||
|     // Get the URL of the first page that contains a source image and the page list | ||||
|     protected String overrideChapterUrl(String defaultPageUrl) { | ||||
|         return defaultPageUrl; | ||||
|     } | ||||
|  | ||||
|     // Get the URL of the pages that contains source images | ||||
|     protected String overridePageUrl(String defaultPageUrl) { | ||||
|         return defaultPageUrl; | ||||
|     } | ||||
|  | ||||
|     // Default headers, it can be overriden by children or just add new keys | ||||
|     protected Headers.Builder headersBuilder() { | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user