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