diff --git a/app/build.gradle b/app/build.gradle index 00aab893e7..9febe45623 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,6 +99,7 @@ dependencies { // Modified dependencies compile 'com.github.inorichi:subsampling-scale-image-view:44aa442' + compile 'com.github.inorichi:junrar-android:634c1f5' // Android support library final support_library_version = '25.1.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fbbcbc1792..3881d5b3d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,11 @@ android:authorities="${applicationId}.zip-provider" android:exported="false" /> + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 0c43ffb56a..d63c908448 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -6,7 +6,10 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.util.ChapterRecognition import eu.kanade.tachiyomi.util.DiskUtil +import eu.kanade.tachiyomi.util.RarContentProvider import eu.kanade.tachiyomi.util.ZipContentProvider +import junrar.Archive +import junrar.rarfile.FileHeader import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import org.jsoup.Jsoup import org.jsoup.nodes.Document @@ -169,7 +172,9 @@ class LocalSource(private val context: Context) : CatalogueSource { } private fun isSupportedFormat(extension: String): Boolean { - return extension.equals("zip", true) || extension.equals("cbz", true) || extension.equals("epub", true) + return extension.equals("zip", true) || extension.equals("cbz", true) + || extension.equals("rar", true) || extension.equals("cbr", true) + || extension.equals("epub", true) } private fun getLoader(file: File): Loader { @@ -180,6 +185,8 @@ class LocalSource(private val context: Context) : CatalogueSource { ZipLoader(file) } else if (extension.equals("epub", true)) { EpubLoader(file) + } else if (extension.equals("rar", true) || extension.equals("cbr", true)) { + RarLoader(file) } else { throw Exception("Invalid chapter format") } @@ -207,8 +214,8 @@ class LocalSource(private val context: Context) : CatalogueSource { class ZipLoader(val file: File) : Loader { override fun load(): List { val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() - ZipFile(file).use { zip -> - return zip.entries().toList() + return ZipFile(file).use { zip -> + zip.entries().toList() .filter { !it.isDirectory && DiskUtil.isImage(it.name, { zip.getInputStream(it) }) } .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.name, f2.name) }) .map { Uri.parse("content://${ZipContentProvider.PROVIDER}${file.absolutePath}!/${it.name}") } @@ -217,6 +224,19 @@ class LocalSource(private val context: Context) : CatalogueSource { } } + class RarLoader(val file: File) : Loader { + override fun load(): List { + val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() + return Archive(file).use { archive -> + archive.fileHeaders + .filter { !it.isDirectory && DiskUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) } + .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) }) + .map { Uri.parse("content://${RarContentProvider.PROVIDER}${file.absolutePath}!-/${it.fileNameString}") } + .mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } } + } + } + } + class EpubLoader(val file: File) : Loader { override fun load(): List { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/RarContentProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/util/RarContentProvider.kt new file mode 100644 index 0000000000..12ad9706f3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/RarContentProvider.kt @@ -0,0 +1,73 @@ +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?, p2: String?, p3: Array?, 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?): Int { + throw UnsupportedOperationException("not implemented") + } + + override fun delete(p0: Uri?, p1: String?, p2: Array?): Int { + throw UnsupportedOperationException("not implemented") + } +} \ No newline at end of file