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:
arkon 2023-11-28 08:59:45 -05:00
parent e41668862f
commit 4fcdde4913
14 changed files with 51 additions and 60 deletions

View File

@ -164,7 +164,6 @@ dependencies {
implementation(compose.ui.tooling.preview) implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.webview) implementation(compose.accompanist.webview)
implementation(compose.accompanist.permissions)
implementation(compose.accompanist.systemuicontroller) implementation(compose.accompanist.systemuicontroller)
lintChecks(compose.lintchecks) lintChecks(compose.lintchecks)

View File

@ -7,9 +7,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- For background jobs --> <!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -39,7 +36,6 @@
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi">

View File

@ -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.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupFileValidator
@ -71,8 +70,6 @@ object SettingsDataScreen : SearchableSettings {
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>() val storagePreferences = Injekt.get<StoragePreferences>()
PermissionRequestHelper.requestStoragePermission()
return listOf( return listOf(
getStorageLocationPref(storagePreferences = storagePreferences), getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),

View File

@ -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()
}
}
}

View File

@ -13,7 +13,6 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
@ -66,9 +65,6 @@ data class BrowseTab(
onChangeSearchQuery = extensionsScreenModel::search, onChangeSearchQuery = extensionsScreenModel::search,
) )
// For local source
PermissionRequestHelper.requestStoragePermission()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true (context as? MainActivity)?.ready = true
} }

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.toTempFile
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -88,13 +89,13 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
is Format.Directory -> DirectoryPageLoader(format.file) is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(format.file) is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
is Format.Rar -> try { is Format.Rar -> try {
RarPageLoader(format.file) RarPageLoader(format.file.toTempFile(context))
} catch (e: UnsupportedRarV5Exception) { } catch (e: UnsupportedRarV5Exception) {
error(context.stringResource(MR.strings.loader_rar5_error)) 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) source is HttpSource -> HttpPageLoader(chapter, source)

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import tachiyomi.core.storage.toTempFile
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -46,7 +47,7 @@ internal class DownloadPageLoader(
} }
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> { 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() return loader.getPages()
} }

View File

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File
/** /**
* Loader used to load a chapter from a .epub 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) private val epub = EpubFile(file)

View File

@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.loader
import com.github.junrar.Archive import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader import com.github.junrar.rarfile.FileHeader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.PipedInputStream import java.io.PipedInputStream
import java.io.PipedOutputStream import java.io.PipedOutputStream
@ -15,9 +14,9 @@ import java.io.PipedOutputStream
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * 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 override var isLocal: Boolean = true

View File

@ -1,24 +1,23 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.os.Build import android.os.Build
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import tachiyomi.core.storage.toFile
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile import java.util.zip.ZipFile
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * 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) { 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 { } else {
ZipFile(file.toFile()) ZipFile(file)
} }
override var isLocal: Boolean = true override var isLocal: Boolean = true

View File

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.util.storage package eu.kanade.tachiyomi.util.storage
import com.hippo.unifile.UniFile
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import tachiyomi.core.storage.toFile
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -13,12 +11,12 @@ import java.util.zip.ZipFile
/** /**
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: UniFile) : Closeable { class EpubFile(file: File) : Closeable {
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = ZipFile(file.toFile()) private val zip = ZipFile(file)
/** /**
* Path separator used by this epub. * Path separator used by this epub.

View File

@ -1,6 +1,10 @@
package tachiyomi.core.storage package tachiyomi.core.storage
import android.content.Context
import android.os.Build
import android.os.FileUtils
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import java.io.BufferedOutputStream
import java.io.File import java.io.File
val UniFile.extension: String? val UniFile.extension: String?
@ -9,4 +13,26 @@ val UniFile.extension: String?
val UniFile.nameWithoutExtension: String? val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.') 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
}

View File

@ -22,7 +22,6 @@ material-core = { module = "androidx.compose.material:material" }
glance = "androidx.glance:glance-appwidget:1.0.0" glance = "androidx.glance:glance-appwidget:1.0.0"
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" } 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" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" } lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }

View File

@ -26,7 +26,7 @@ import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.storage.extension import tachiyomi.core.storage.extension
import tachiyomi.core.storage.nameWithoutExtension 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.lang.withIOContext
import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -213,7 +213,7 @@ actual class LocalSource(
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
ZipFile(chapter.toFile()).use { zip: ZipFile -> ZipFile(chapter.toTempFile(context)).use { zip: ZipFile ->
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
zip.getInputStream(comicInfoFile).buffered().use { stream -> zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@ -222,7 +222,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { 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.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream -> rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath) return copyComicInfoFile(stream, folderPath)
@ -272,7 +272,7 @@ actual class LocalSource(
val format = Format.valueOf(chapterFile) val format = Format.valueOf(chapterFile)
if (format is Format.Epub) { if (format is Format.Epub) {
EpubFile(format.file).use { epub -> EpubFile(format.file.toTempFile(context)).use { epub ->
epub.fillMetadata(manga, this) epub.fillMetadata(manga, this)
} }
} }
@ -331,7 +331,7 @@ actual class LocalSource(
entry?.let { coverManager.update(manga, it.openInputStream()) } entry?.let { coverManager.update(manga, it.openInputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file.toFile()).use { zip -> ZipFile(format.file.toTempFile(context)).use { zip ->
val entry = zip.entries().toList() val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
@ -340,7 +340,7 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(format.file.toFile()).use { archive -> JunrarArchive(format.file.toTempFile(context)).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
@ -349,7 +349,7 @@ actual class LocalSource(
} }
} }
is Format.Epub -> { is Format.Epub -> {
EpubFile(format.file).use { epub -> EpubFile(format.file.toTempFile(context)).use { epub ->
val entry = epub.getImagesFromPages() val entry = epub.getImagesFromPages()
.firstOrNull() .firstOrNull()
?.let { epub.getEntry(it) } ?.let { epub.getEntry(it) }