New reader (#1550)

* Delete old reader

* Add utility methods

* Update dependencies

* Add new reader

* Update tracking services. Extract transition strings into resources

* Restore delete read chapters

* Documentation and some minor changes

* Remove content providers for compressed files, they are not needed anymore

* Update subsampling. New changes allow to parse magic numbers and decode tiles with a single stream. Drop support for custom image decoders. Other minor fixes
This commit is contained in:
inorichi
2018-09-01 17:12:59 +02:00
committed by GitHub
parent 7c99ae1b3b
commit 18f89cc341
105 changed files with 6918 additions and 7247 deletions

View File

@@ -11,6 +11,7 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.PowerManager
import android.support.annotation.AttrRes
import android.support.annotation.StringRes
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
@@ -79,7 +80,7 @@ fun Context.hasPermission(permission: String)
*
* @param resource the attribute.
*/
fun Context.getResourceColor(@StringRes resource: Int): Int {
fun Context.getResourceColor(@AttrRes resource: Int): Int {
val typedArray = obtainStyledAttributes(intArrayOf(resource))
val attrValue = typedArray.getColor(0, 0)
typedArray.recycle()
@@ -161,4 +162,4 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
@Suppress("DEPRECATION")
return manager.getRunningServices(Integer.MAX_VALUE)
.any { className == it.service.className }
}
}

View File

@@ -8,47 +8,9 @@ import android.os.Environment
import android.support.v4.content.ContextCompat
import android.support.v4.os.EnvironmentCompat
import java.io.File
import java.io.InputStream
import java.net.URLConnection
object DiskUtil {
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
val contentType = try {
URLConnection.guessContentTypeFromName(name)
} catch (e: Exception) {
null
} ?: openStream?.let { findImageMime(it) }
return contentType?.startsWith("image/") ?: false
}
fun findImageMime(openStream: () -> InputStream): String? {
try {
openStream().buffered().use {
val bytes = ByteArray(8)
it.mark(bytes.size)
val length = it.read(bytes, 0, bytes.size)
it.reset()
if (length == -1)
return null
if (bytes[0] == 'G'.toByte() && bytes[1] == 'I'.toByte() && bytes[2] == 'F'.toByte() && bytes[3] == '8'.toByte()) {
return "image/gif"
} else if (bytes[0] == 0x89.toByte() && bytes[1] == 0x50.toByte() && bytes[2] == 0x4E.toByte()
&& bytes[3] == 0x47.toByte() && bytes[4] == 0x0D.toByte() && bytes[5] == 0x0A.toByte()
&& bytes[6] == 0x1A.toByte() && bytes[7] == 0x0A.toByte()) {
return "image/png"
} else if (bytes[0] == 0xFF.toByte() && bytes[1] == 0xD8.toByte() && bytes[2] == 0xFF.toByte()) {
return "image/jpeg"
} else if (bytes[0] == 'W'.toByte() && bytes[1] == 'E'.toByte() && bytes[2] == 'B'.toByte() && bytes[3] == 'P'.toByte()) {
return "image/webp"
}
}
} catch(e: Exception) {
}
return null
}
fun hashKeyForDisk(key: String): String {
return Hash.md5(key)
}

View File

@@ -0,0 +1,117 @@
package eu.kanade.tachiyomi.util
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
* Wrapper over ZipFile to load files in epub format.
*/
class EpubFile(file: File) : Closeable {
/**
* Zip file of this epub.
*/
private val zip = ZipFile(file)
/**
* Closes the underlying zip file.
*/
override fun close() {
zip.close()
}
/**
* Returns an input stream for reading the contents of the specified zip file entry.
*/
fun getInputStream(entry: ZipEntry): InputStream {
return zip.getInputStream(entry)
}
/**
* Returns the zip file entry for the specified name, or null if not found.
*/
fun getEntry(name: String): ZipEntry? {
return zip.getEntry(name)
}
/**
* Returns the path of all the images found in the epub file.
*/
fun getImagesFromPages(): List<String> {
val allEntries = zip.entries().toList()
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val pages = getPagesFromDocument(doc)
val hrefs = getHrefMap(ref, allEntries.map { it.name })
return getImagesFromPages(pages, hrefs)
}
/**
* Returns the path to the package document.
*/
private fun getPackageHref(): String {
val meta = zip.getEntry("META-INF/container.xml")
if (meta != null) {
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
if (path != null) {
return path
}
}
return "OEBPS/content.opf"
}
/**
* Returns the package document where all the files are listed.
*/
private fun getPackageDocument(ref: String): Document {
val entry = zip.getEntry(ref)
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
}
/**
* Returns all the pages from the epub.
*/
private fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { "application/xhtml+xml" == it.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") }
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
}
/**
* Returns all the images contained in every page from the epub.
*/
private fun getImagesFromPages(pages: List<String>, hrefs: Map<String, String>): List<String> {
return pages.map { page ->
val entry = zip.getEntry(hrefs[page])
val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
document.getElementsByTag("img").mapNotNull { hrefs[it.attr("src")] }
}.flatten()
}
/**
* Returns a map with a relative url as key and abolute url as path.
*/
private fun getHrefMap(packageHref: String, entries: List<String>): Map<String, String> {
val lastSlashPos = packageHref.lastIndexOf('/')
if (lastSlashPos < 0) {
return entries.associateBy { it }
}
return entries.associateBy { entry ->
if (entry.isNotBlank() && entry.length > lastSlashPos) {
entry.substring(lastSlashPos + 1)
} else {
entry
}
}
}
}

View File

@@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.util
import java.io.InputStream
import java.net.URLConnection
object ImageUtil {
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
try {
val guessedMime = URLConnection.guessContentTypeFromName(name)
if (guessedMime.startsWith("image/")) {
return true
}
} catch (e: Exception) {
/* Ignore error */
}
return openStream?.let { findImageType(it) } != null
}
fun findImageType(openStream: () -> InputStream): ImageType? {
return openStream().use { findImageType(it) }
}
fun findImageType(stream: InputStream): ImageType? {
try {
val bytes = ByteArray(8)
val length = if (stream.markSupported()) {
stream.mark(bytes.size)
stream.read(bytes, 0, bytes.size).also { stream.reset() }
} else {
stream.read(bytes, 0, bytes.size)
}
if (length == -1)
return null
if (bytes.compareWith(charByteArrayOf(0xFF, 0xD8, 0xFF))) {
return ImageType.JPG
}
if (bytes.compareWith(charByteArrayOf(0x89, 0x50, 0x4E, 0x47))) {
return ImageType.PNG
}
if (bytes.compareWith("GIF8".toByteArray())) {
return ImageType.GIF
}
if (bytes.compareWith("RIFF".toByteArray())) {
return ImageType.WEBP
}
} catch(e: Exception) {
}
return null
}
private fun ByteArray.compareWith(magic: ByteArray): Boolean {
for (i in 0 until magic.size) {
if (this[i] != magic[i]) return false
}
return true
}
private fun charByteArrayOf(vararg bytes: Int): ByteArray {
return ByteArray(bytes.size).apply {
for (i in 0 until bytes.size) {
set(i, bytes[i].toByte())
}
}
}
enum class ImageType(val mime: String, val extension: String) {
JPG("image/jpeg", "jpg"),
PNG("image/png", "png"),
GIF("image/gif", "gif"),
WEBP("image/webp", "webp")
}
}

View File

@@ -1,73 +0,0 @@
package eu.kanade.tachiyomi.util
import android.content.ContentProvider
import android.content.ContentValues
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import eu.kanade.tachiyomi.BuildConfig
import junrar.Archive
import java.io.File
import java.io.IOException
import java.net.URLConnection
import java.util.concurrent.Executors
class RarContentProvider : ContentProvider() {
private val pool by lazy { Executors.newCachedThreadPool() }
companion object {
const val PROVIDER = "${BuildConfig.APPLICATION_ID}.rar-provider"
}
override fun onCreate(): Boolean {
return true
}
override fun getType(uri: Uri): String? {
return URLConnection.guessContentTypeFromName(uri.toString())
}
override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor? {
try {
val pipe = ParcelFileDescriptor.createPipe()
pool.execute {
try {
val (rar, file) = uri.toString()
.substringAfter("content://$PROVIDER")
.split("!-/", limit = 2)
Archive(File(rar)).use { archive ->
val fileHeader = archive.fileHeaders.first { it.fileNameString == file }
ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]).use { output ->
archive.extractFile(fileHeader, output)
}
}
} catch (e: Exception) {
// Ignore
}
}
return AssetFileDescriptor(pipe[0], 0, -1)
} catch (e: IOException) {
return null
}
}
override fun query(p0: Uri?, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?): Cursor? {
return null
}
override fun insert(p0: Uri?, p1: ContentValues?): Uri {
throw UnsupportedOperationException("not implemented")
}
override fun update(p0: Uri?, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
throw UnsupportedOperationException("not implemented")
}
override fun delete(p0: Uri?, p1: String?, p2: Array<out String>?): Int {
throw UnsupportedOperationException("not implemented")
}
}

View File

@@ -10,4 +10,8 @@ operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(
fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
return Observable.combineLatest(this, o2, combineFn)
}
}
fun Subscription.addTo(subscriptions: CompositeSubscription) {
subscriptions.add(this)
}

View File

@@ -1,69 +0,0 @@
package eu.kanade.tachiyomi.util
import android.content.ContentProvider
import android.content.ContentValues
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import eu.kanade.tachiyomi.BuildConfig
import java.io.IOException
import java.net.URL
import java.net.URLConnection
import java.util.concurrent.Executors
class ZipContentProvider : ContentProvider() {
private val pool by lazy { Executors.newCachedThreadPool() }
companion object {
const val PROVIDER = "${BuildConfig.APPLICATION_ID}.zip-provider"
}
override fun onCreate(): Boolean {
return true
}
override fun getType(uri: Uri): String? {
return URLConnection.guessContentTypeFromName(uri.toString())
}
override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor? {
try {
val url = "jar:file://" + uri.toString().substringAfter("content://$PROVIDER")
val input = URL(url).openStream()
val pipe = ParcelFileDescriptor.createPipe()
pool.execute {
try {
val output = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])
input.use {
output.use {
input.copyTo(output)
}
}
} catch (e: IOException) {
// Ignore
}
}
return AssetFileDescriptor(pipe[0], 0, -1)
} catch (e: IOException) {
return null
}
}
override fun query(p0: Uri?, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?): Cursor? {
return null
}
override fun insert(p0: Uri?, p1: ContentValues?): Uri {
throw UnsupportedOperationException("not implemented")
}
override fun update(p0: Uri?, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
throw UnsupportedOperationException("not implemented")
}
override fun delete(p0: Uri?, p1: String?, p2: Array<out String>?): Int {
throw UnsupportedOperationException("not implemented")
}
}