Bladeren bron

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.
arkon 1 jaar geleden
bovenliggende
commit
4fcdde4913

+ 0 - 1
app/build.gradle.kts

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

+ 0 - 4
app/src/main/AndroidManifest.xml

@@ -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">

+ 0 - 3
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt

@@ -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)),

+ 0 - 20
app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt

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

+ 0 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt

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

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt

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

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt

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

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt

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

+ 3 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt

@@ -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
 

+ 4 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt

@@ -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

+ 2 - 4
core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt

@@ -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.

+ 27 - 1
core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt

@@ -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
+}

+ 0 - 1
gradle/compose.versions.toml

@@ -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" }

+ 7 - 7
source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt

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