Remove storage permissions
Requires adjusting some file reading to first copy to a temporary file in cache that we have permissions to read from. This is only applicable for things like ZIP files where we need an actual File rather than just some Android content URI shenanigans.
This commit is contained in:
parent
e41668862f
commit
4fcdde4913
14 changed files with 51 additions and 60 deletions
|
@ -164,7 +164,6 @@ dependencies {
|
|||
implementation(compose.ui.tooling.preview)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.webview)
|
||||
implementation(compose.accompanist.permissions)
|
||||
implementation(compose.accompanist.systemuicontroller)
|
||||
lintChecks(compose.lintchecks)
|
||||
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
@ -39,7 +36,6 @@
|
|||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
|
|
|
@ -35,7 +35,6 @@ import eu.kanade.presentation.more.settings.Preference
|
|||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.permissions.PermissionRequestHelper
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
|
@ -71,8 +70,6 @@ object SettingsDataScreen : SearchableSettings {
|
|||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||
|
||||
PermissionRequestHelper.requestStoragePermission()
|
||||
|
||||
return listOf(
|
||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
package eu.kanade.presentation.permissions
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
/**
|
||||
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
||||
*/
|
||||
object PermissionRequestHelper {
|
||||
|
||||
@Composable
|
||||
fun requestStoragePermission() {
|
||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ import cafe.adriel.voyager.navigator.Navigator
|
|||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.components.TabbedScreen
|
||||
import eu.kanade.presentation.permissions.PermissionRequestHelper
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
|
@ -66,9 +65,6 @@ data class BrowseTab(
|
|||
onChangeSearchQuery = extensionsScreenModel::search,
|
||||
)
|
||||
|
||||
// For local source
|
||||
PermissionRequestHelper.requestStoragePermission()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
(context as? MainActivity)?.ready = true
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.storage.toTempFile
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
@ -88,13 +89,13 @@ class ChapterLoader(
|
|||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||
is Format.Zip -> ZipPageLoader(format.file)
|
||||
is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
|
||||
is Format.Rar -> try {
|
||||
RarPageLoader(format.file)
|
||||
RarPageLoader(format.file.toTempFile(context))
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
error(context.stringResource(MR.strings.loader_rar5_error))
|
||||
}
|
||||
is Format.Epub -> EpubPageLoader(format.file)
|
||||
is Format.Epub -> EpubPageLoader(format.file.toTempFile(context))
|
||||
}
|
||||
}
|
||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
|
|
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import tachiyomi.core.storage.toTempFile
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
@ -46,7 +47,7 @@ internal class DownloadPageLoader(
|
|||
}
|
||||
|
||||
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
||||
val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it }
|
||||
val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
|
||||
return loader.getPages()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .epub file.
|
||||
*/
|
||||
internal class EpubPageLoader(file: UniFile) : PageLoader() {
|
||||
internal class EpubPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val epub = EpubFile(file)
|
||||
|
||||
|
|
|
@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||
|
||||
import com.github.junrar.Archive
|
||||
import com.github.junrar.rarfile.FileHeader
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import tachiyomi.core.storage.toFile
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
|
@ -15,9 +14,9 @@ import java.io.PipedOutputStream
|
|||
/**
|
||||
* Loader used to load a chapter from a .rar or .cbr file.
|
||||
*/
|
||||
internal class RarPageLoader(file: UniFile) : PageLoader() {
|
||||
internal class RarPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val rar = Archive(file.toFile())
|
||||
private val rar = Archive(file)
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.os.Build
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import tachiyomi.core.storage.toFile
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
*/
|
||||
internal class ZipPageLoader(file: UniFile) : PageLoader() {
|
||||
internal class ZipPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
ZipFile(file.toFile(), StandardCharsets.ISO_8859_1)
|
||||
ZipFile(file, StandardCharsets.ISO_8859_1)
|
||||
} else {
|
||||
ZipFile(file.toFile())
|
||||
ZipFile(file)
|
||||
}
|
||||
|
||||
override var isLocal: Boolean = true
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import com.hippo.unifile.UniFile
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import tachiyomi.core.storage.toFile
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
@ -13,12 +11,12 @@ import java.util.zip.ZipFile
|
|||
/**
|
||||
* Wrapper over ZipFile to load files in epub format.
|
||||
*/
|
||||
class EpubFile(file: UniFile) : Closeable {
|
||||
class EpubFile(file: File) : Closeable {
|
||||
|
||||
/**
|
||||
* Zip file of this epub.
|
||||
*/
|
||||
private val zip = ZipFile(file.toFile())
|
||||
private val zip = ZipFile(file)
|
||||
|
||||
/**
|
||||
* Path separator used by this epub.
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package tachiyomi.core.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.FileUtils
|
||||
import com.hippo.unifile.UniFile
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
|
||||
val UniFile.extension: String?
|
||||
|
@ -9,4 +13,26 @@ val UniFile.extension: String?
|
|||
val UniFile.nameWithoutExtension: String?
|
||||
get() = name?.substringBeforeLast('.')
|
||||
|
||||
fun UniFile.toFile(): File? = filePath?.let { File(it) }
|
||||
fun UniFile.toTempFile(context: Context): File {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)!!
|
||||
val tempFile = File.createTempFile(
|
||||
nameWithoutExtension.orEmpty(),
|
||||
null,
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
FileUtils.copy(inputStream, tempFile.outputStream())
|
||||
} else {
|
||||
BufferedOutputStream(tempFile.outputStream()).use { tmpOut ->
|
||||
inputStream.use { input ->
|
||||
val buffer = ByteArray(8192)
|
||||
var count: Int
|
||||
while (input.read(buffer).also { count = it } > 0) {
|
||||
tmpOut.write(buffer, 0, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ material-core = { module = "androidx.compose.material:material" }
|
|||
glance = "androidx.glance:glance-appwidget:1.0.0"
|
||||
|
||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
||||
|
||||
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }
|
|
@ -26,7 +26,7 @@ import tachiyomi.core.metadata.comicinfo.getComicInfo
|
|||
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
||||
import tachiyomi.core.storage.extension
|
||||
import tachiyomi.core.storage.nameWithoutExtension
|
||||
import tachiyomi.core.storage.toFile
|
||||
import tachiyomi.core.storage.toTempFile
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
|
@ -213,7 +213,7 @@ actual class LocalSource(
|
|||
for (chapter in chapterArchives) {
|
||||
when (Format.valueOf(chapter)) {
|
||||
is Format.Zip -> {
|
||||
ZipFile(chapter.toFile()).use { zip: ZipFile ->
|
||||
ZipFile(chapter.toTempFile(context)).use { zip: ZipFile ->
|
||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
|
@ -222,7 +222,7 @@ actual class LocalSource(
|
|||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
JunrarArchive(chapter.toFile()).use { rar ->
|
||||
JunrarArchive(chapter.toTempFile(context)).use { rar ->
|
||||
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
||||
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
|
@ -272,7 +272,7 @@ actual class LocalSource(
|
|||
|
||||
val format = Format.valueOf(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
EpubFile(format.file.toTempFile(context)).use { epub ->
|
||||
epub.fillMetadata(manga, this)
|
||||
}
|
||||
}
|
||||
|
@ -331,7 +331,7 @@ actual class LocalSource(
|
|||
entry?.let { coverManager.update(manga, it.openInputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file.toFile()).use { zip ->
|
||||
ZipFile(format.file.toTempFile(context)).use { zip ->
|
||||
val entry = zip.entries().toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
@ -340,7 +340,7 @@ actual class LocalSource(
|
|||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
JunrarArchive(format.file.toFile()).use { archive ->
|
||||
JunrarArchive(format.file.toTempFile(context)).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
@ -349,7 +349,7 @@ actual class LocalSource(
|
|||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
EpubFile(format.file.toTempFile(context)).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
|
Reference in a new issue