Skip to content

Commit a6c19ef

Browse files
fix: add legacy storage permission check for saving covers
Request WRITE_EXTERNAL_STORAGE permission on Android < Q when saving manga covers or reader images. Added rememberLegacyStoragePermissionState composable to track permission state and show dialog when needed.
1 parent e930f38 commit a6c19ef

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

app/src/main/java/eu/kanade/presentation/util/Permissions.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package eu.kanade.presentation.util
22

3+
import android.Manifest
4+
import android.content.pm.PackageManager
5+
import android.os.Build
36
import androidx.compose.runtime.Composable
47
import androidx.compose.runtime.DisposableEffect
58
import androidx.compose.runtime.getValue
69
import androidx.compose.runtime.mutableStateOf
710
import androidx.compose.runtime.remember
811
import androidx.compose.runtime.setValue
912
import androidx.compose.ui.platform.LocalContext
13+
import androidx.core.content.ContextCompat
1014
import androidx.lifecycle.DefaultLifecycleObserver
1115
import androidx.lifecycle.LifecycleOwner
1216
import androidx.lifecycle.compose.LocalLifecycleOwner
1317

18+
const val LEGACY_STORAGE_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE
19+
1420
@Composable
1521
fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean {
1622
val context = LocalContext.current
@@ -32,3 +38,26 @@ fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false)
3238

3339
return installGranted
3440
}
41+
42+
@Composable
43+
fun rememberLegacyStoragePermissionState(initialValue: Boolean = false): Boolean {
44+
val context = LocalContext.current
45+
val lifecycleOwner = LocalLifecycleOwner.current
46+
47+
var storageGranted by remember { mutableStateOf(initialValue) }
48+
49+
DisposableEffect(lifecycleOwner.lifecycle) {
50+
val observer = object : DefaultLifecycleObserver {
51+
override fun onResume(owner: LifecycleOwner) {
52+
storageGranted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
53+
ContextCompat.checkSelfPermission(context, LEGACY_STORAGE_PERMISSION) == PackageManager.PERMISSION_GRANTED
54+
}
55+
}
56+
lifecycleOwner.lifecycle.addObserver(observer)
57+
onDispose {
58+
lifecycleOwner.lifecycle.removeObserver(observer)
59+
}
60+
}
61+
62+
return storageGranted
63+
}

app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import eu.kanade.presentation.manga.components.SetIntervalDialog
3939
import eu.kanade.presentation.util.AssistContentScreen
4040
import eu.kanade.presentation.util.Screen
4141
import eu.kanade.presentation.util.isTabletUi
42+
import eu.kanade.presentation.util.LEGACY_STORAGE_PERMISSION
43+
import eu.kanade.presentation.util.rememberLegacyStoragePermissionState
4244
import eu.kanade.tachiyomi.source.Source
4345
import eu.kanade.tachiyomi.source.isLocalOrStub
4446
import eu.kanade.tachiyomi.source.online.HttpSource
@@ -62,6 +64,7 @@ import tachiyomi.core.common.util.lang.withIOContext
6264
import tachiyomi.core.common.util.system.logcat
6365
import tachiyomi.domain.chapter.model.Chapter
6466
import tachiyomi.domain.manga.model.Manga
67+
import tachiyomi.i18n.MR
6568
import tachiyomi.presentation.core.screens.LoadingScreen
6669

6770
class MangaScreen(
@@ -244,16 +247,31 @@ class MangaScreen(
244247
val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) }
245248
val manga by sm.state.collectAsState()
246249
if (manga != null) {
250+
val hasStoragePermission = rememberLegacyStoragePermissionState()
247251
val getContent = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
248252
if (it == null) return@rememberLauncherForActivityResult
249253
sm.editCover(context, it)
250254
}
255+
val requestStoragePermission =
256+
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
257+
if (granted) {
258+
sm.saveCover(context)
259+
} else {
260+
context.toast(MR.strings.missing_storage_permission)
261+
}
262+
}
251263
MangaCoverDialog(
252264
manga = manga!!,
253265
snackbarHostState = sm.snackbarHostState,
254266
isCustomCover = remember(manga) { manga!!.hasCustomCover() },
255267
onShareClick = { sm.shareCover(context) },
256-
onSaveClick = { sm.saveCover(context) },
268+
onSaveClick = {
269+
if (hasStoragePermission) {
270+
sm.saveCover(context)
271+
} else {
272+
requestStoragePermission.launch(LEGACY_STORAGE_PERMISSION)
273+
}
274+
},
257275
onEditClick = {
258276
when (it) {
259277
EditCoverAction.EDIT -> getContent.launch("image/*")

app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import android.view.View
1919
import android.view.View.LAYER_TYPE_HARDWARE
2020
import android.view.WindowManager
2121
import android.widget.Toast
22+
import androidx.activity.compose.rememberLauncherForActivityResult
2223
import androidx.activity.enableEdgeToEdge
24+
import androidx.activity.result.contract.ActivityResultContracts
2325
import androidx.activity.viewModels
2426
import androidx.compose.foundation.layout.Arrangement
2527
import androidx.compose.foundation.layout.Box
@@ -57,6 +59,8 @@ import eu.kanade.presentation.reader.ReaderPageIndicator
5759
import eu.kanade.presentation.reader.ReadingModeSelectDialog
5860
import eu.kanade.presentation.reader.appbars.ReaderAppBars
5961
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
62+
import eu.kanade.presentation.util.LEGACY_STORAGE_PERMISSION
63+
import eu.kanade.presentation.util.rememberLegacyStoragePermissionState
6064
import eu.kanade.tachiyomi.R
6165
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
6266
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@@ -256,6 +260,15 @@ class ReaderActivity : BaseActivity() {
256260
onChangeOrientation = viewModel::setMangaOrientationType,
257261
)
258262
}
263+
val hasStoragePermission = rememberLegacyStoragePermissionState()
264+
val requestStoragePermission =
265+
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
266+
if (granted) {
267+
viewModel.saveImage()
268+
} else {
269+
toast(MR.strings.missing_storage_permission)
270+
}
271+
}
259272

260273
Box(modifier = Modifier.fillMaxSize()) {
261274
if (!state.menuVisible && showPageNumber) {
@@ -325,7 +338,13 @@ class ReaderActivity : BaseActivity() {
325338
onDismissRequest = onDismissRequest,
326339
onSetAsCover = viewModel::setAsCover,
327340
onShare = viewModel::shareImage,
328-
onSave = viewModel::saveImage,
341+
onSave = {
342+
if (hasStoragePermission) {
343+
viewModel.saveImage()
344+
} else {
345+
requestStoragePermission.launch(LEGACY_STORAGE_PERMISSION)
346+
}
347+
},
329348
)
330349
}
331350
null -> {}

0 commit comments

Comments
 (0)